일반적으로 각 앱은 하나의 프로세스로 실행되며 자신의 프로세스에서 사용하는 데이터는 자신만 접근 가능하다. 하지만 사진이나 연락처등 정보를 가져와야 하는 경우 ContentProvider는 앱 내의 데이터(데이터베이스, 파일)를 다른 앱에서도 접근할 수 있도록 통로를 제공해주는 역할을 하며, 보안을 위해 안드로이드 시스템에서 관리하며 Manifest 파일에 명시해줘야 시스템에서 알 수 있다.

ContentProvider가 생성된 다른 애플리케이션의 데이터에 접근하기 위해선 Context에 있는ContentResolver 객체를 사용하여 ContentProvider와 서버-클라이언트 구조로 통신을 주고받아야 한다.

UI에서 ContentProvider에 접근하는 일반적인 패턴은 CursorLoader를 사용하여 백그라운드에서 비동기식 쿼리를 실행하는 방식으로, 쿼리를 진행하는 동안 UI를 계속 제공할 수 있다. 위 그림처럼 Activity 또는 Fragment가 CursorLoader를 호출하고 ContentResolver를 사용하여 ContentProvider와 통신을 한다.
ContentProvider는 데이터를 CRUD 방식으로 처리하며 ContentProvider를 사용하려면 아래와 같은 구조의 URI를 기반으로 데이터에 접근한다.
content://<authority>/<table>/<id>
content:// : ContentProvider 식별자authority : 특정 ContentProvider를 식별하는 고유 이름table : 접근하려는 데이터 (Table or Collection)id : 특정 데이터의 고유 ID (Optional)<provider
android:name=".MyContentProvider"
android:authorities="com.example.myprovider"
android:exported="true"
android:grantUriPermissions="true" />
authorities : ContentProvider를 식별하는 고유 이름exported : 외부 앱에서 접근 가능 여부 (true / false)grantUriPermissions : 특정 앱에 데이터 접근 권한을 동적으로 부여할지 여부class MyContentProvider : ContentProvider() {
companion object {
const val AUTHORITY = "com.example.myprovider"
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/users")
}
private lateinit var databaseHelper: MyDatabaseHelper
override fun onCreate(): Boolean {
databaseHelper = MyDatabaseHelper(context!!)
return true
}
override fun query(
uri: Uri, // 요청된 데이터의 URI
projection: Array<String>?, // 조회할 컬럼 목록 (null이면 모든 컬럼 조회)
selection: String?, // WHERE 절 (Optional)
selectionArgs: Array<String>?, // WHERE 절의 값
sortOrder: String? // 정렬 기준 (예: "name ASC")
): Cursor? {
val db = databaseHelper.readableDatabase
return db.query("users", projection, selection, selectionArgs, null, null, sortOrder)
}
override fun insert(
uri: Uri, // 삽입할 테이블 URI
values: ContentValues? // 삽입할 데이터
): Uri? {
val db = databaseHelper.writableDatabase
val id = db.insert("users", null, values)
return Uri.withAppendedPath(CONTENT_URI, id.toString())
}
override fun update(
uri: Uri, // 업데이트할 테이블 URI
values: ContentValues?, // 변경할 데이터
selection: String?, // WHERE 절 (Optional)
selectionArgs: Array<String>? // WHERE 절의 값
): Int {
val db = databaseHelper.writableDatabase
return db.update("users", values, selection, selectionArgs)
}
override fun delete(
uri: Uri, // 삭제할 데이터의 URI
selection: String?, // WHERE 절 (Optional)
selectionArgs: Array<String>? // WHERE 절의 값
): Int {
val db = databaseHelper.writableDatabase
return db.delete("users", selection, selectionArgs)
}
override fun getType(uri: Uri): String? {
// "vnd.android.cursor.dir/vnd.$AUTHORITY.users" : 여러개 데이터
// "vnd.android.cursor.item/vnd.$AUTHORITY.users" : 단일 데이터
return "vnd.android.cursor.dir/vnd.$AUTHORITY.users"
}
}
onCreate : ContentProvider가 처음 생성될 때 호출query : 데이터 조회 (SELECT)insert : 데이터 추가 (INSERT)update : 데이터 수정 (UPDATE)delete : 데이터 삭제 (DELETE)getType : MIME type 반환val cursor = contentResolver.query(
Uri.parse("content://com.example.myprovider/users"),
null, null, null, null
)
cursor?.use {
while (it.moveToNext()) {
val name = it.getString(it.getColumnIndexOrThrow("name"))
Log.d("ContentProvider", "User Name: $name")
}
}
val values = ContentValues().apply {
put("name", "Jong Tang")
put("email", "jong@example.com")
}
val newUserUri = contentResolver.insert(
Uri.parse("content://com.example.myprovider/users"), values
)
Log.d("ContentProvider", "New User URI: $newUserUri")
val values = ContentValues().apply {
put("email", "new_email@example.com")
}
val rowsUpdated = contentResolver.update(
Uri.parse("content://com.example.myprovider/users"),
values,
"name = ?",
arrayOf("Jong Tang")
)
Log.d("ContentProvider", "Rows Updated: $rowsUpdated")
val rowsDeleted = contentResolver.delete(
Uri.parse("content://com.example.myprovider/users"),
"name = ?",
arrayOf("Jong Tang")
)
Log.d("ContentProvider", "Rows Deleted: $rowsDeleted")
val uri = Uri.parse("content://com.example.myprovider/users/1")
val packageName = "com.otherapp.example"
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
다만 권한은 영구적으로 유지되지 않으며, 앱이 다시 시작되면 권한이 사라질 수 있다는점을 유의해야한다.
FLAG_GRANT_READ_URI_PERMISSION : 읽기 권한 허용FLAG_GRANT_WRITE_URI_PERMISSION : 쓰기 권한 허용FileProvider는 내부 파일을 외부 앱과 안전하게 공유할 수 있도록 도와주는 ContentProvider의 한 종류이다.
Manifest에 FileProvider 등록<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
res/xml/file_paths.xml 공유 경로 지정<paths>
<external-files-path name="shared_images" path="Pictures/" />
</paths>
FileProvider로 파일 공유val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "image.jpg")
val uri = FileProvider.getUriForFile(context, "com.example.fileprovider", file)
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.type = "image/*"
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(Intent.createChooser(shareIntent, "이미지 공유하기"))
grantUriPermissions를 동해 특정 URI에 대한 일시적 권한을 부여하여 다른 앱이 image.jpg 파일 읽기 가능