다른 앱으로 데이터를 제공하고자 할 경우 사용한다.
ContentProvider 만드는 이유
ContentResolver
ContentResolver를 받아오면 해당 레퍼런스를 이용해 query 메서드 호출
URI : 데이터의 위치를 표시하고 데이터를 가져오기 위해 사용
다른 Query Prameter
// var uri = "content://com.android.contacts/contacts/1"
var uri = ContactsContract.Contacts.CONTENT_URI.toString() + "1" //1번째 연락처를 가져오기 위해 uri 주소를 만들어줌
var uri2 = Uri.parse(uri) // string 주소값을 실제 uri 로 바꿈
contentResolver.query(uri2 , null, null, null, null)
URI는 안드로이드 시스템에서 상수로 매핑되어있기 때문에 주석 처리된 문을 사용하나, 아래의 문을 사용하나 같은 의미이다.
쿼리 string을 작성하여 구체적인 데이터 값을 가져와보자
첫번째 데이터를 가져와 이 행 레이블들을 출력해주면
레이블들을 확인해 볼 수 있다.
var name = it.getString(it.getColumnIndexOrThrow("display_name"))
getColumnIndexOrThrow
는 해당 열이 없으면 오류를 출력한다.
val URI = ContactsContract.Contacts.CONTENT_URI // 전체 주소록
val cursor = contentResolver.query(URI, null, null, null, null)
val from = arrayOf( "_id", "display_name")
val to = intArrayOf( R.id.id_item, R.id.name_item)
val adapter1 = SimpleCursorAdapter(
this,
R.layout.list_item,
cursor!!,
from,
to,
FLAG_REGISTER_CONTENT_OBSERVER
)
listview.adapter = adapter1
contentResolver로 전체 주소록의 값을 가져왔다. 이는 Cursor 객체로 저장되는데,
CursorAdapter
를 사용하면 (예시에서는 cursorAdapter을 상속받는 simpleCursorAdapter 사용) 아이템을 넣을 레이아웃, 커서, 넣을 컬럼명, 넣어질 레이아웃 안의 뷰 값, observer 값을 넣어주어서 adapter를 만들 수 있다.
FLAG_REGISTER_CONTENT_OBSERVER
는
콘텐트 프로바이더의 내용이 바뀔 때마다(데이터 변경이 있을 때마다) 콜백 메서드로 onContentChanged()
를 호출한다.
simpleCursorAdapter 대신에 이를 상속받는 MySimpleCursorAdapter를 만들었다.
//상속해서 구현하면 주소록 변경시 onContentChanged 콜백됨.
inner class MySimpleCursorAdapter(
context: Context, layout:Int, cursor:Cursor, from:Array<String>, to:IntArray, flag:Int
) : SimpleCursorAdapter(context, layout, cursor,from, to, flag){
override fun onContentChanged() {
super.onContentChanged()
Log.d(TAG, "onContentChanged: ")
// cursor.requery()
val newCursor = contentResolver.query(URI, null, null, null, null)
swapCursor(newCursor)
}
}
onContentChanged를 오버라이딩 하면 FLAG_REGISTER_CONTENT_OBSERVER 로 설정한 adapter가 content provider를 통해 받아오는 주소값이 변경되었을 때 onContentChanged 를 불러 오버라이딩 한 대로 adapter를 재구성 해준다.
//android version 대응. 10이상.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//정렬 & 건수제한
//bundle은 쿼리의 개수 제한, 순서 등등의 queryArgs 를 담아준다
val bundle = Bundle().apply {
putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(MediaStore.Images.ImageColumns.DATE_TAKEN))
putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING)
putInt(ContentResolver.QUERY_ARG_OFFSET, 0)
putInt(ContentResolver.QUERY_ARG_LIMIT, 3)
}
return resolver.query(queryUri, what, bundle, null)
10 버전 이상부터는 bundle에 속성을 넣은 다음 쿼리 문에 담는 식으로 처리한다.
또한 커서에서 행을 하나하나 탐색할 때는 일반적으로 가장 첫번째 인덱스로 간다음 계속 인덱스가 있을 때까지 moveNext로 다음 인덱스로 접근한다. 이를 위해 do-while 패턴을 주로 사용한다.
do {
//열 인덱스로 데이터 구하기.
val id = cursor.getLong(idColNum)
val title = cursor.getString(titleColNum)
val dateTaken = cursor.getLong(dateTakenColNum)
val imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
Log.d(TAG, "imageUri: ${imageUri}") // content://media/external/images/media/31
Log.d(TAG, "ISO: ${cursor.getString(4)}")
val text = DateFormat.format("yyyy/MM/dd (E) kk:mm:ss", Date(dateTaken)).toString()
//View에 데이터 세팅
var textId = getResources().getIdentifier("textView_" + (++index), "id", "${packageName}");
var textView = findViewById<TextView>(textId);
var imgId = getResources().getIdentifier("imageView_" + index, "id", "${packageName}");
var imgView = findViewById<ImageView>(imgId);
textView.text = "촬영일시: $text"
imgView.setImageURI(imageUri)
} while (cursor.moveToNext())
Contentprovider에서 쿼리를 호출할 때 URI를 구성한다. 이 때 마지막 분이 /notes
notes/3
일 때 실행문이 다를 것이다.
/notes
: 전체 조회
notes/3
: 단건 조회
이에 대하여 uri의 생김새로 분기를 태워주도록 하는 것이 uriMatcher이다.
private var sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply{
addURI(Notes.AUTHORITY, "notes", NOTES)
addURI(Notes.AUTHORITY, "notes/#", NOTE_ID)
}
override fun getType(uri: Uri): String? {
return when (sUriMatcher.match(uri)) {
NOTES -> CONTENT_TYPE
NOTE_ID -> CONTENT_ITEM_TYPE
else -> throw IllegalArgumentException("Unknown URI $uri")
}
}