ContentProvider를 이용한 앱 간 데이터 공유(ft. Room db) - ContentProvider 생성 [2]

Heathcliff·2022년 8월 12일

ContentProvider 선언 및 접근 제한

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" />
  • authorities : ContentProvider의 식별하는 속성, Provider에 접근하기 위해 URI 생성 시 해당정보 를 입력
  • readPermission/writePermission : 외부에서 현재 앱 DB에 접근 가능한 권한을 설정한다.
    패키지명.READ_DATABASE / 패키지명.WRITE_DATABASE
  • 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" />

RoomDatabase 설정

아래와 같이 id, title, content 세가지 컬럼이 있는 Table을 추가한다.

@Entity(tableName = "item")
data class Item(

    @PrimaryKey(autoGenerate = true)
    var itemId: Long = 0L,
    var title: String = "",
    var content: String = ""

)

CRUD를 위한 Dao를 아래와 같이 정의한다. * Select문에서 반환값은 Cursor로 반환한다. * update, delete문에서 반환값은 변경, 삭제한 item의 수이다.
@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
}

CotentProvider 클래스 구현

ContentProvider를 상속하는 사용자 Provider를 생성한다.
기본적으로 다음 메소드를 오버라이드 해야 한다.

  • onCreate()
  • query()
  • insert()
  • update()
  • delete()
  • getTypte()

onCreate, getType을 제외하고는 내부DB에 CRUD하는 메서드이다.
우리는 RoomDB를 활용하여 데이터를 관리 할 것이다.


onCreate()

onCreate()에서 DB 싱글톤 객체를 가져와 전역변수 db에 초기화 해준다.

lateinit var db: ItemDatabase

override fun onCreate(): Boolean {

	context?.let { db = ItemDatabase.getInstance(it) }
	return true
}

query()

데이터를 조회하는 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")
    }

cursor를 통한 데이터 탐색

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")
}

ContentResolver 구현

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앱에서 ContentValues를 생성후 데이터를 담고 이 데이터를 A앱에 전달해 A앱 내부 DB에 데이터를 insert하도록 요청하는 코드이다.

아래의 코드처럼 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
}

ContnetProvider <-> ContentResolver 간 커스텀 메서드

위에서 본것 처럼 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앱에서 필요한 권한

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앱 전체코드 링크

profile
Android Developer

0개의 댓글