ContentProvider는 AndroidManifest.xml 의 application 영역에 아래와 같이 정의를 해준다.
<provider
android:name=".provider.MyContentProvider"
android:authorities="com.study.providera.MyContentProvider"
android:readPermission="com.study.providera.READ_DATABASE"
android:writePermission="com.study.providera.WRITE_DATABAS"
android:exported="true" />
ContentProvider에서 사용하는 read/wirete 퍼미션을 AndroidManifest.xml 에 아래와 같이 정의한다.
<permission android:name="com.study.providera.READ_DATABASE" android:protectionLevel="normal" />
<permission android:name="com.study.providera.WRITE_DATABASE" android:protectionLevel="normal" />
아래와 같이 id, title, content 세가지 컬럼이 있는 Table을 추가한다.
@Entity(tableName = "item")
data class Item(
@PrimaryKey(autoGenerate = true)
var itemId: Long = 0L,
var title: String = "",
var content: String = ""
)
@Dao
interface ItemDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertItem(item: Item): Long
@Update
fun updateItem(item: Item): Int
@Query("DELETE FROM item WHERE itemId = :id")
fun deleteItem(id: Long): Int
@Query("DELETE FROM item")
fun deleteAll(): Int
@Query("SELECT * FROM item WHERE itemId = :id")
fun getItem(id: Long): Cursor
@Query("SELECT * FROM item")
fun getAllItem(): Cursor
}
ContentProvider를 상속하는 사용자 Provider를 생성한다.
기본적으로 다음 메소드를 오버라이드 해야 한다.
onCreate, getType을 제외하고는 내부DB에 CRUD하는 메서드이다.
우리는 RoomDB를 활용하여 데이터를 관리 할 것이다.
onCreate()에서 DB 싱글톤 객체를 가져와 전역변수 db에 초기화 해준다.
lateinit var db: ItemDatabase
override fun onCreate(): Boolean {
context?.let { db = ItemDatabase.getInstance(it) }
return true
}
데이터를 조회하는 query() 메서드를 아래와 같이 구현한다.
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?,
): Cursor? {
context?.let {
val cursor = db.itemDao().getAllItem()
cursor.setNotificationUri(it.contentResolver, uri)
return cursor
}
throw IllegalArgumentException("Failed to query row for uri $uri")
}
query() 메서드에서 받는 cursor객체로 데이터를 가져오는 코드는 아래와 같다.
Room에서 정의한 table의 컬럼인 'itemId', 'title', 'content' 의 ColumnIndex로 각각 type에 맞는 데이터를 가져온다.
while (cursor.moveToNext()) {
val itemIdIndex = cursor.getColumnIndex("itemId")
val titleIndex = cursor.getColumnIndex("title")
val contentIndex = cursor.getColumnIndex("content")
val id = cursor.getLong(itemIdIndex)
val title = cursor.getString(titleIndex)
val content = cursor.getString(contentIndex)
val data = "id[$id] : $title - $content"
Log.v(">>>", "data : $data")
}
다음으로 그밖의 getType(), insert(), delete(), update() 메서드를 구현한다.
getType()은 사용자가 임의로 반환하고 싶은 정보를 넣어도 무방하다.
override fun getType(p0: Uri): String? {
return "${MyContract.AUTHORITY}.${MyContract.TABLE_NAME}"
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
context?.let {
val id = db.itemDao().insertItem(Item.fromContentValues(values))
if (id != -1L) {
it.contentResolver.notifyChange(uri, null)
return ContentUris.withAppendedId(uri, id)
}
}
throw IllegalArgumentException("Failed to insert row into $uri")
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
context?.let {
val count = db.itemDao().deleteItem(ContentUris.parseId(uri))
it.contentResolver.notifyChange(uri, null)
return count
}
throw IllegalArgumentException("Failed to delete row into $uri")
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
context?.let {
val count = db.itemDao().updateItem(Item.fromContentValues(values))
it.contentResolver.notifyChange(uri, null)
return count
}
throw IllegalArgumentException("Failed to update row into $uri")
}
B앱 에서는 A앱의 데이터를 공유 받기 위해 ContentResolver를 사용해 ContentProvider에서 구현한 query(), insert(), update(), delete() 를 호출하거나 커스텀 메서드를 호출하여 데이터에 접근한다.
B앱이 CotentResolver를 사용해서 A앱의 ContentProvider에게 요청하기 위해서는 URI정보가 필요한데 관련 내용은 아래와 같이 정의한다.
object MyContract {
const val TABLE_NAME = "item"
const val AUTHORITY = "com.study.providera.MyContentProvider"
const val URI_STRING = "content://$AUTHORITY/$TABLE_NAME"
val CONTENT_URI: Uri = Uri.parse(URI_STRING)
}
이때 AUTHOIRTY는 A앱 Manifest에서 Provider 정의 시 입력한 authorities 속성의 값과 일치해야한다.
아래는 ContentResolver의 query() 메서드를 사용해 A앱의 ContentProvider에서 구현한 query()가 호출하여 A앱의 내부DB에서 Item List를 가져오도록 요청하는 코드이다.
private var contentResolver: ContentResolver = mContext.contentResolver
fun getAllItems() {
val cursor = contentResolver.query(MyContract.CONTENT_URI, null, null, null, null)
if (cursor != null && cursor.count > 0) {
while (cursor.moveToNext()) {
val itemIdIndex = cursor.getColumnIndex("itemId")
val titleIndex = cursor.getColumnIndex("title")
val contentIndex = cursor.getColumnIndex("content")
val id = cursor.getLong(itemIdIndex)
val title = cursor.getString(titleIndex)
val content = cursor.getString(contentIndex)
Log.v(">>>", "@# id[$id] title[$title] content[$content]")
}
}
}
아래의 코드처럼 B앱에서 contentResolver.insert()를 호출하면 A앱의 ContentProvider에서 구현한 insert()메서드가 호출된다.
/**
* Insert
*/
fun insertItem(title: String, content: String) {
val contentValues = generateItem(title, content)
contentResolver.insert(MyContract.CONTENT_URI, contentValues)
}
/**
* Item 생성 (ContentValues)
*/
private fun generateItem(title: String, content: String): ContentValues {
val values = ContentValues()
values.put("title", title)
values.put("content", content)
return values
}
위에서 본것 처럼 query(), insert(), update(), delete()와 같은 메서드를 호출하여 기본적인 CRUD를 할 수 있지만 A앱과 B앱간에 커서텀 메서드를 정의하여 B앱에서 A앱의 커스텀 메서드를 호출하고 그에 따른 리턴값도 받을 수 있다.
일단 A앱에 ContentProvider에서 아래와 같이 커스텀 메서드를 정의한다. (커스텀 메서드를 사용하기 위해서는 ContentProvider의 call() 메서드를 구현해야한다.)
/**
* Custom Method
*/
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
val bundle = Bundle()
if (method == "getId") {
bundle.putString("id", "robot123")
return bundle
}
return null
}
call 메서드에서 'method' 파라미터로 메서드를 분기하여 각 메서드에 맞는 return값을 보내준다. 예제에서는 "getId"라는 문자열이 'method' 값으로 왔으므로 bundle을 생성하여 키 : "id" 값 : "robot123" 인 정보를 bundle에 넣어 반환한다.
이제 B앱의 관점이다.
B앱 에서는 A앱의 ContentProvider call() 메서드를 호출 하기위하여 ContentResolver를 사용해서 call() 메서드를 아래와 같이 호출한다.
/**
* 커스텀 메서드 - id 가져오기
*/
fun customMethodGetId(): String? {
var value: String? = null
val bundle: Bundle? = contentResolver.call(MyContract.CONTENT_URI, "getId", null, null)
bundle?.let {
val id = it.getString("id")
Log.v(">>>", "customMethodGetId : $id")
value = id
}
return value
}
contentResolver의 call 메서드를 호출하며 URI정보, "getId" 문자열로 메서드명을 인자로 전달한다.
B앱에서 A앱의 ContentProvider에 접근을 하기 위한 권한을 AndroidManifest.xml에 정의를 해야한다.
그 내용은 아래와 같이 정의한다.
<uses-permission android:name="com.study.providera.READ_DATABASE"/>
<uses-permission android:name="com.study.providera.WRITE_DATABASE"/>
<queries>
<package android:name="com.study.providera"/>
</queries>
A앱과 B앱의 전체코드 링크는 아래에서 확인이 가능하다.
A앱 전체코드 링크
B앱 전체코드 링크