컨텐츠 제공자 만들기

kosdjs·2025년 7월 12일

Android

목록 보기
16/29
  • 컨텐츠 제공자는 데이터의 중앙 저장소에 대한 접근을 관리함, 제공자는 하나 이상의 클래스를 구현해야하며 이를 매니페스트 파일의 요소로 추가해야 함, 이 클래스 중 하나의 클래스는 ContentProvider 의 자식 클래스를 구현해야하며 이는 다른 앱과 제공자 사이의 인터페이스임

  • 다른 앱이 데이터를 사용 가능하게 하는것이 컨텐츠 제공자의 목적이지만, 앱 내에 제공자에 의해 관리되는 데이터를 사용자가 조회하거나 수정할 수 있는 액티비티를 가질 수 있음

빌드 전 알아야 할 사항

  • 컨텐츠 제공자가 필요한지 결정해야 함, 다음과 같은 기능이 필요하다면 컨텐츠 제공자를 만들어야 함
    • 다른 앱에 복잡한 데이터나 파일을 제공해야 할 때
    • 사용자가 복잡한 데이터를 다른 앱으로 복사해야 할 때
    • 검색 프레임워크를 사용해 추천 검색어를 제공해야 할 때
    • 위젯에서 앱 데이터를 필요로 할 때
    • AbstractThreadedSyncAdapter, CursorAdapter, CursorLoader 클래스를 구현해야 할 때
  • 데이터 베이스나 다른 종류의 저장소를 본인의 앱만을 위해 사용하거나 위에서 언급한 기능이 필요하지 않다면 제공자가 필요하지 않음

제공자를 빌드하는 과정

  1. 데이터를 위한 저장소를 설계
  • 파일 데이터: 사진, 오디오, 비디오와 같은 데이터는 일반적으로 파일임, 파일을 앱의 개인 공간에 저장하면 다른 앱이 파일을 요청했을 때 제공자가 파일을 처리해 제공할 수 있음
  • 구조화된 데이터: 배열, 데이터베이스 또는 비슷한 구조에 저장되는 데이터, 행과 열로 이루어진 테이블로 된 구조에 데이터를 저장함, 행은 사람 또는 인벤토리의 아이템처럼 엔티티를 뜻함, 열은 사람의 이름이나 아이템의 가격과 같이 엔티티의 일부 데이터를 뜻함, 이런 데이터를 일반적으로 저장하는 방법은 SQLite 데이터베이스를 사용하는 방법이 있음, 다른 유형의 영구 저장소도 사용 가능함
  1. ContentProvider 클래스와 그 클래스에서 필요한 메소드를 구체적으로 구현, 이 클래스는 데이터와 나머지 안드로이드 시스템간의 인터페이스임

  2. 제공자의 authority 문자열, 컨텐츠 URI, 열 이름 정의, 제공자의 앱이 인텐트를 처리해야 한다면 인텐트 동작, 추가 데이터, 플래그도 정의해야 함, 또한 앱이 데이터에 접근할 때 필요한 권한도 정의해야 함, 이런 값들을 별도의 계약 클래스의 상수로 정의해 다른 개발자가 알 수 있게 하면 좋음

  3. 샘플 데이터나 제공자와 클라우드 기반 데이터를 동기화하는 AbstractThreadedSyncAdapter 의 구현체와 같은 선택적 요소 추가

데이터 저장소 설계

  • 컨텐츠 제공자가 구조화된 데이터를 사용하기 위한 인터페이스이므로, 이런 인터페이스를 생성하기 전에 데이터를 어떻게 저장할 지 결정하고 인터페이스를 설계해야 함

안드로이드에서 사용 가능한 일부 데이터 저장소 기술

  • 구조화된 데이터로 작업을 한다면 SQLite와 같은 관계형 데이터베이스나 LevelDB와 같은 비관계형 키-값 쌍 데이터베이스를 고려할 수 있고 오디오, 이미지, 비디오와 같은 구조화되지 않은 데이터로 작업을 한다면 파일로 저장하는 것을 고려할 수 있음, 필요하다면 여러 저장소를 혼합해 사용할 수 있음

  • 테이블 기반 데이터를 저장하기 위해 SQLite 데이터베이스 접근 API를 제공하는 Room을 사용할 수 있음, RoomDatabase 의 자식 클래스를 인스턴스화 시켜 데이터베이스를 생성함

  • 데이터베이스를 사용하지 않아도 됨, 외부에서는 관계형 데이터베이스처럼 테이블로 보이지만 제공자 구현의 필수 조건은 아님

  • 안드로이드는 파일을 저장하기 위한 다양한 파일 기반 API가 있음, 음악이나 영상과 같이 미디어에 관계된 데이터를 제공자를 설계한다면 테이블 기반 데이터와 파일을 함께 활용하는 방식으로 구현 가능

  • 드문 경우지만 앱에서 두 개 이상의 컨텐츠 제공자를 구현할 수 있음, 예시로 위젯과 데이터를 공유하기 위해 하나의 컨텐츠 제공자를 사용하고, 나머지 데이터를 다른 앱과 공유하기 위해 다른 컨텐츠 제공자를 사용할 수 있음

  • java.netandroid.net 에 있는 클래스를 이용해 네트워크 기반 데이터를 작업할 수 있음, 네트워크 기반 데이터를 데이터베이스와 같은 로컬 데이터 저장소에 동기화한 후 테이블이나 파일로 데이터를 제공할 수도 있음

노트: 만약 저장소를 이전 버전과 호환되지 않게 변경한다면, 저장소를 새로운 버전의 번호로 표시해야 합니다. 또한 새 컨텐츠 제공자를 구현하는 앱의 버전 번호도 증가시켜야 합니다. 이런 변경을 하면, 시스템이 호환되지 않는 컨텐츠 제공자를 가진 앱을 다시 설치하려 할 때 시스템 다운그레이드로 인해 발생할 수 있는 충돌을 예방할 수 있습니다.

데이터 설계 고려사항

  • 테이블 데이터는 각 행마다 고유한 번호 값을 유지하기 위해 반드시 기본 키 열을 가지고 있어야 함, 이 값을 외래키로 사용해 다른 테이블의 관련된 행과 연결하는 데 사용할 수 있음, 이 열의 이름을 아무렇게나 사용해도 괜찮지만 BaseColumns._ID 로 사용하는 것이 가장 좋음, ListView 에 제공자의 검색 결과를 연결하려면 _ID 라고 이름된 열이 필요하기 때문

  • 만약 비트맵 이미지나 아주 큰 조각의 파일 기반 데이터를 제공하고 싶다면, 테이블에 저장하기보다 파일에 저장하고 제공하는 것이 좋음, 이 방법을 사용했다면 사용자에게 ContentResolver 의 파일 메소드를 사용해야 데이터에 접근할 수 있다는 것을 알려야 함

  • 크기가 다양하거나 구조가 다양한 데이터를 저장하기 위해 binary large object(BLOB) 타입을 사용할 수 있음, BLOB 열을 사용해 프로토콜 버퍼나 JSON 구조를 저장할 수 있음, 또한 BLOB 를 사용하면 스키마에 독립적인 테이블을 구현할 수 있음, 테이블에 기본 키 열, MIME 유형 열, 하나 이상의 BLOB 열을 정의하고 MIME 유형 열에 BLOB 열에 저장된 데이터의 유형을 저장해 테이블에 행마다 다른 유형의 데이터를 저장할 수 있음, 예시로 연락처 제공자의 data 테이블인 ContactsContract.Data 를 예시로 들 수 있음

컨텐츠 URI 설계

  • 컨텐츠 URI는 제공자의 데이터를 식별하는 URI임, authority 라 불리는 제공자의 상징적인 이름과 경로라고 불리는 테이블 또는 파일을 가리키는 이름을 포함해야 함, 선택사항인 ID 부분은 테이블 내의 개별 행을 가리킴, ContentProvider 의 모든 데이터 접근 메소드는 접근할 테이블, 행, 파일을 결정하기 위해 컨텐츠 URI를 인수로 가지고 있음

authority 설계

  • 제공자는 일반적으로 안드로이드 내부 이름의 역할을 하는 단일의 authority 를 가짐, 다른 제공자와의 충돌을 피하려면 인터넷 도메인 소유권(역순으로)을 authority 의 기반으로 사용하는 게 좋음, 안드로이드 패키지 이름에도 이런 추천이 적용되기 때문에 제공자 authority 를 제공자를 포함하는 패키지 이름의 확장으로 정의할 수 있음

  • 예시로 패키지 이름이 com.example.<appname> 이라면 제공자의 authoritycom.example.<appname>.provider

경로 구조 설계

  • 개발자는 일반적으로 authority 에 경로를 추가해 개별 테이블을 가리키는 컨텐츠 URI를 생성함, 앞서 살펴봤던 authority 의 예시와 두 테이블을 결합하면 컨텐츠 URI를 com.example.provider.<appname>/table1com.example.<appname>.provider/table2 와 같이 예시로 들 수 있음, 경로는 단일 부분으로 제한되어 있지 않으므로 경로의 각 레벨이 테이블일 필요는 없음

컨텐츠 URI ID 처리

  • 관레적으로, 제공자는 URI의 끝에 행의 ID 값이 있는 컨텐츠 URI를 허용해 테이블의 단일 행에 접근을 제공함, 또한 제공자는 행에 대한 접근을 수행하기 위해 테이블의 _ID 열과 ID 값을 맞춤

  • 이런 관례는 앱이 제공자에 접근하는 공통 디자인 패턴을 가능하게 함, 앱은 제공자에 대한 검색 결과인 CursorListView 에 표시하기 위해 CursorAdapter 를 사용함, CursorAdapterCursor_ID 라는 이름의 열이 필요함

  • 사용자가 데이터를 보거나 수정하기 위해 UI에 표시된 행 중 하나를 선택하면, 앱은 ListView 를 지원하는 Cursor 에서 해당 행을 가져오고 _ID 값을 가져옴, 그 후 컨텐츠 URI에 추가해 접근 요청을 제공자로 보냄, 그러면 제공자가 사용자가 정한 행을 검색하거나 수정할 수 있음

컨텐츠 URI 패턴

  • 수신되는 컨텐츠 URI에 대해 어떤 행동을 취할지 선택하는데 도움이 되도록 제공자 API는 컨텐츠 URI 패턴을 정수 값으로 매핑하는 UriMatcher 클래스를 포함함, 정수 값을 switch 구문을 이용해 컨텐츠 URI나 특정 URI 패턴에 대해 취해야 할 행동을 선택할 수 있음

  • 컨텐츠 URI 패턴에는 다음과 같은 와일드카드 문자를 사용해 컨텐츠 URI를 맞출 수 있음

    • * : 모든 길이의 모든 유효한 문자로 이루어진 문자열과 맞춰짐
    • # : 모든 길이의 숫자 문자로 이루어진 문자열과 맞춰짐
  • 제공자의 authoritycom.example.app.provider 일 때 테이블을 가리키는 컨텐츠 URI의 예시

    • content://com.example.app.provider/table1: table1 테이블
    • content://com.example.app.provider/table2/dataset1: dataset1 테이블
    • content://com.example.app.provider/table2/dataset2: dataset2 테이블
    • content://com.example.app.provider/table3: table3 테이블
  • 위와 같은 컨텐츠 URI를 인식하는 제공자는 table3 의 1번 행을 나타내는 URI content://com.example.app.provider/table3/1 와 같이 행 ID가 추가된 컨텐츠 URI도 인식함

  • 이럴 때 컨텐츠 URI 패턴의 예시

    • content://com.example.app.provider/*: 제공자의 모든 컨텐츠 URI와 일치함
    • content://com.example.app.provider/table2/*: dataset1dataset2 테이블의 컨텐츠 URI와 일치하지만, table1table3 테이블의 컨텐츠 URI와는 일치하지 않음
    • content://com.example.app.provider/table3/#: 6번 행을 나타내는 컨텐츠 URI(content://com.example.app.provider/table3/6)와 같이 table3 테이블의 단일 행의 컨텐츠 URI와 일치함

addURI() 메소드로 authority 와 경로를 정수 값으로 매핑하고, match() 메소드로 URI를 정수 값으로 반환해 전체 테이블과 단일 행을 switch 구문으로 선택해 처리하는 예시

private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    /*
     * 제공자가 인식하는 모든 콘텐츠 URI 패턴에 대한 addURI() 호출이 여기에 옵니다.
     * 이 코드에서는 table3에 대한 호출만 표시됩니다.
     */

    /*
     * table3의 여러 행에 대한 정수 값을 1로 설정합니다.
     * 경로에 와일드카드가 사용되지 않은 것을 확인하세요.
     */
    addURI("com.example.app.provider", "table3", 1)

    /*
     * 단일 행에 대한 코드를 2로 설정합니다. 이 경우 # 와일드카드가 사용됩니다.
     * content://com.example.app.provider/table3/3 은 일치하지만
     * content://com.example.app.provider/table3 은 일치하지 않습니다.
     */
    addURI("com.example.app.provider", "table3/#", 2)
}
...
class ExampleProvider : ContentProvider() {
    ...
    // ContentProvider.query()를 구현합니다.
    override fun query(
            uri: Uri?,
            projection: Array<out String>?,
            selection: String?,
            selectionArgs: Array<out String>?,
            sortOrder: String?
    ): Cursor? {
        var localSortOrder: String = sortOrder ?: ""
        var localSelection: String = selection ?: ""
        when (sUriMatcher.match(uri)) {
            1 -> { // 수신된 URI가 table3 전체에 대한 것이라면
                if (localSortOrder.isEmpty()) {
                    localSortOrder = "_ID ASC"
                }
            }
            2 -> {  // 수신된 URI가 단일 행에 대한 것이라면
                /*
                 * 이 URI는 단일 행에 대한 것이므로 _ID 값 부분이 있습니다.
                 * URI에서 마지막 경로 세그먼트를 가져옵니다. 이것이 _ID 값입니다.
                 * 그런 다음 쿼리의 WHERE 절에 값을 추가합니다.
                 */
                localSelection += "_ID ${uri?.lastPathSegment}"
            }
            else -> { // URI가 인식되지 않으면
                // 여기서 일부 오류 처리를 수행합니다.
            }
        }

        // 실제로 쿼리를 수행하는 코드를 호출합니다.
    }
}
  • ContentUris 는 컨텐츠 URI의 id 부분을 작업하는 편리한 메소드를 제공하는 클래스, UriUri.Builder 클래스들은 존재하는 Uri 객체를 파싱하거나 새로운 URI를 만드는데 편리한 메소드를 포함함

ContentProvider 클래스 구현

  • ContentProvider 인스턴스는 다른 앱의 요청을 처리해 구조화된 데이터셋에 대한 접근을 관리함, 접근 요청을 할 때 ContentResolver 를 호출하면 데이터에 접근하기 위해 ContentResolver 에서 ContentProvider 의 구체적인 메소드를 호출함

필요한 메소드

  • ContentProvider 추상 클래스는 구체적인 자식 클래스를 구현하기 위해 6가지 추상 메소드를 정의함, onCreate() 를 제외한 나머지 메소드들은 컨텐츠 제공자에 접근하려는 클라이언트 앱에서 호출됨

  • query(): 제공자로부터 데이터를 검색하고 Cursor 객체로 데이터를 반환함, 인수를 사용해 검색할 테이블 선택, 반환할 행과 열, 결과의 정렬 순서를 결정함

  • insert(): 제공자에 새로운 행을 삽입하고 그 행에 대한 컨텐츠 URI를 반환함, 인수를 사용해 목적지 테이블을 선택하고 사용할 열의 값을 가져옴

  • update(): 제공자의 존재하는 행을 수정하고 수정한 행의 수를 반환함, 인수를 사용해 수정할 테이블과 행을 선택하고 수정할 열의 값을 가져옴

  • delete(): 제공자에서 행을 삭제하고 삭제한 행의 수를 반환함, 인수를 사용해 테이블과 삭제할 행을 선택함

  • getType(): 컨텐츠 URI에 대응하는 MIME 유형을 반환함

  • onCreate(): 제공자를 초기화함, 안드로이드 시스템에서 제공자가 생성된 직후에 이 메소드를 호출함, 제공자는 ContentResolver 객체가 접근하기 전까지 생성되지 않음

  • 이 메소드들은 같은 이름을 가지는 ContentResolver 의 메소드와 동일한 시그니처를 가지고 있음, 즉 메소드의 이름과 매개변수가 동일하다는 뜻임

메소드 구현 시 고려사항

  • onCreate() 메소드를 제외한 나머지 메소드들은 동시에 다양한 스레드에서 한번에 호출될 수 있으므로 스레드로부터 안전하게 설계되어야 함

  • onCreate() 에서 오래걸리는 작업을 피할 것, 초기화 작업은 실제로 필요해지기 전까지 연기해야 함

  • 이런 메소드를 반드시 구현해야 하지만, 코드가 실제로는 예상된 데이터 유형을 반환하는 것 외에는 아무것도 해야할 필요가 없음, 예시로 다른 앱에서 테이블에 데이터를 삽입하는 것을 막고 싶다면 insert() 호출을 무시하고 0을 반환하면 됨

query() 메소드 구현

  • ContentProvider.query() 메소드는 Cursor 객체를 반환하거나 실패했을 때 Exception 이 발생함, 데이터 저장소로 SQLite 데이터베이스를 사용중이라면 SQLiteDatabase 클레스의 query() 메소드에서 반환되는 Cursor 를 반환할 수 있음

  • 만약 어떤 행도 검색되지 않는다면 getCount() 메소드가 0을 반환하는 Cursor 인스턴스를 반환해야 함, 검색 과정에서 내부 오류가 발생했을 때만 null 을 반환함

  • 데이터 저장소로 SQLite 데이터베이스를 사용하지 않는다면 Cursor 의 구체적인 자식 클래스를 사용하면 됨, 예시로 MatrixCursor 클래스는 각 행이 Object 인스턴스의 배열인 커서를 구현한 클래스임, addRow() 를 사용해 새 행을 추가할 수 있음

  • 안드로이드 시스템은 프로세스 경계 간에서 Exception 을 통신할 수 있어야 함, 안드로이드는 검색 오류를 처리하는 데 유용한 다음과 같은 예외를 사용해 통신할 수 있음

    • IllegalArgumentException: 제공자가 유효하지 않은 컨텐츠 URI를 전달받았을 때 발생시킬 수 있음
    • NullPointerException

insert() 메소드 구현

  • insert() 메소드는 ContentValues 인수에 있는 값을 사용해 적합한 테이블에 새 행을 추가함, ContentValues 인수에 열 이름이 존재하지 않는다면 제공자의 코드나 데이터베이스 스키마에서 기본 값을 가져옴

  • 이 메소드는 새 행에 대한 컨텐츠 URI를 반환함, 이 컨텐츠 URI를 만들기 위해서는 새 행의 기본키(보통 _ID 값)를 withAppendedId() 를 사용해 테이블의 컨텐츠 URI에 추가하면 됨

delete() 메소드 구현

  • delete() 메소드는 데이터 저장소의 행을 지울 필요는 없음, 제공자에 동기화 어댑터를 사용중이라면 행을 완전히 삭제하기보다 행에 delete 플래그를 표시하는 것을 고려할 수 있음, 그러면 동기화 어댑터는 삭제된 행을 확인해 제공자에서 먼저 제거하기 전에 서버에서 먼저 제거할 수 있음

update() 메소드 구현

  • update() 메소드는 insert() 에서 사용되는 것과 동일한 ContentValues 인수를 받고 delete()ContentProvider.query() 에서 사용되는 selection, selectionArgs 인수를 동일하게 받음, 이는 메소드간 재사용을 할 수 있게 함

onCreate() 메소드 구현

  • 안드로이드 시스템은 onCreate() 를 제공자를 시작할 때 호출함, 이 메소드에서는 빠르게 실행되는 초기화 작업만 실행되어야 하고 데이터베이스 생성이나 데이터 로딩은 제공자가 실제로 데이터에 대한 요청을 받을때까지 연기되어야 함, 만약 onCreate() 에서 시간이 걸리는 작업을 한다면 제공자 시작을 느리게 만들고 이는 제공자가 다른 앱에게 응답을 보내는 것을 느리게 만듬

ContentProvider.onCreate() 구현 예시

// 데이터베이스 이름을 정의합니다.
private const val DBNAME = "mydb"
...
class ExampleProvider : ContentProvider() {

    // Room 데이터베이스에 대한 핸들을 정의합니다.
    private lateinit var appDatabase: AppDatabase

    // 데이터베이스 작업을 수행할 데이터 액세스 객체를 정의합니다.
    private var userDao: UserDao? = null

    override fun onCreate(): Boolean {

        // 새 데이터베이스 객체를 생성합니다.
        appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, DBNAME).build()

        // 데이터베이스 작업을 수행할 데이터 액세스 객체를 가져옵니다.
        userDao = appDatabase.userDao

        return true
    }
    ...
    // 제공자의 insert 메서드를 구현합니다.
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        // 데이터를 삽입할 때 사용할 DAO를 결정하고, 오류 조건을 처리하는 등의 코드를 여기에 삽입합니다.
    }
}

컨텐츠 제공자 MIME 유형 구현

  • ContentProvider 클래스는 MIME 유형을 반환하는 두가지 메소드가 있음
    • getType() : 모든 제공자가 구현해야 하는 필수 메소드
    • getStreamTypes() : 파일을 제공할 때 구현해야 하는 메소드

테이블의 MIME 유형

  • getType() 메소드는 컨텐츠 URI 인수에 의해 반환되는 데이터의 유형을 MIME 형식의 문자열로 반환함, Uri 인수는 구체적인 URI가 아닌 패턴이 될 수 있음, 이런 경우에 패턴과 일치하는 컨텐츠 URI와 관련된 데이터의 유형을 반환해야 함

  • text, HTML, JPEG 와 같은 평범한 데이텨 유형일 때 getType() 은 그 데이터의 표준 MIME 유형을 반환함

  • 테이블 데이터의 하나 이상의 행을 가리키고 있는 컨텐츠 URI일 때 getType() 는 MIME 유형을 안드로이드 공급업체별 MIME 형식으로 반환함

    • 유형 부분: vnd
    • 하위 유형 부분: URI 패턴이 단일 행을 나타낸다면 android.cursor.item/, 여러 행을 나타낸다면 android.cursor.dir/
    • 제공자별 부분: vnd.<name>.<type> <name> 값은 전역적으로 고유해야 하고, <type> 값은 대응되는 URI 패턴에 대해 고유해야 함, <name> 을 회사의 이름이나 앱의 패키지 이름의 일부로 짓는 것이 좋음, <type> 은 URI와 관련된 테이블을 식별할 수 있는 문자열로 하는 것이 좋음

제공자의 authoritycom.example.app.provider 이고 테이블이 table1 일 때 테이블의 여러 행일 때의 MIME 유형 예시

vnd.android.cursor.dir/vnd.com.example.provider.table1

table1 의 단일행의 MIME 유형 예시

vnd.android.cursor.item/vnd.com.example.provider.table1

파일의 MIME 유형

  • 제공자가 파일을 제공한다면 제공자가 컨텐츠 URI에 대해 반환할 수 있는 파일들의 MIME 유형들의 문자열 배열을 반환하는 getStreamTypes() 메소드를 구현해야 함

  • 클라이언트가 처리를 원하는 MIME 유형들만 반환하기 위해 MIME 유형 필터 인수를 전달할 수 있음

  • 예시로 JPG, PNG, GIF 형식의 사진 이미지 파일을 제공하는 제공자가 있을 때, 앱에서 이미지의 형식을 알기 위해 ContentResolver.getStreamTypes() 메소드를 image/* 라는 필터 문자열로 호출했다면 다음과 같이 배열이 반환됨

{ "image/jpeg", "image/png", "image/gif"}
  • 만약 JPG 파일에만 관심이 있다면 *\/jpeg 라는 필터 문자열로 ContentResolver.getStreamTypes() 를 호출할 수 있고 이 때는 다음과 같이 반환됨
{"image/jpeg"}
  • 제공자가 필터 문자열에서 요청하는 어떤 MIME 유형도 제공하지 않는다면 getStreamTypes()null 을 반환함

계약 클래스 구현

  • 계약 클래스는 URI, 열 이름, MIME 유형과 제공자에 관련된 메타 데이터들을 정의하는 상수를 포함하는 public final 클래스임, 이 클래스는 URI, 열 이름 등의 실제 값이 변경되어도 제공자에 정확하게 접근할 수 있도록 제공자와 다른 앱간의 계약을 설정함

  • 계약 클래스는 일반적으로 상수에 대해 기억하기 쉬운 이름을 가지고 있어 개발자가 URI와 열 이름에 대해 잘못된 값을 사용할 확률을 줄여주기 때문에 도움이 됨, 또한 클래스이므로 Javadoc 문서를 포함할 수 있으므로 Android Studio와 같은 통합 개발 환경에서 계약 클래스로부터 상수를 자동 완성할 수 있고, 상수에 대해 문서를 표시해줄 수 있음

  • 개발자는 앱에서 계약 클래스의 클래스 파일에 직접 접근할 수 없지만, 제공되는 JAR 파일을 통해 클래스를 정적으로 컴파일해서 앱에 포함시킬 수 있음, 즉 이를 통해 개발자가 계약 클래스를 제공받아 상수를 사용할 수 있고, 이를 통해 실제 URI나 열 이름이 변경되어도 상수로 접근하기 때문에 데이터에 제대로 접근할 수 있음

컨텐츠 제공자 권한 구현

  • 기본적으로 기기의 내부 저장소에 저장되는 데이터 파일은 그 파일을 저장하는 앱과 그 앱의 제공자만 사용할 수 있음

  • 생성한 SQLiteDatabase 데이터베이스는 생성한 앱과 그 앱의 제공자만 사용할 수 있음

  • 기본적으로 외부 저장소에 저장하는 데이터 파일은 다른 앱이 파일을 읽고 쓰기 위해 API를 호출해 접근할 수 있는 공개 상태이기 때문에 컨텐츠 제공자를 외부 저장소의 파일로 접근하는 수단으로 제한하지 못함

  • 기기 내부 저장소의 파일이나 SQLite 데이터베이스를 생성하거나 여는 메소드 호출은 다른 앱들에게 읽기 및 쓰기 권한을 잠재적으로 부여할 가능성이 있음, 만약 제공자의 저장소로 사용하는 내부의 파일이나 데이터베이스에 world-readable 또는 world-writeable 을 부여하면 제공자를 통하지 않아도 다른 앱에서 저장소에 직접 접근할 수 있기 때문에 제공자를 위해 매니페스트에 설정한 권한이 데이터를 보호하지 못할 수 있음, 내부 저장소의 파일이나 데이터베이스의 기본 설정이 외부에서 접근하지 못하는 것이므로 제공자의 저장소로 사용하기 위해 이 설정을 바꾸지 않아야 함

  • 컨텐츠 제공자의 권한이 데이터에 접근하는 것을 제어하게 하려면 데이터를 내부 파일이나 SQLite 데이터베이스, 원격 서버와 같은 클라우드에 저장하고 파일이나 데이터베이스를 외부에서 접근이 불가능하게 만들어야 함

권한 구현

  • 기본적으로 제공자는 권한이 없기 때문에 데이터가 공개되어 있지 않아도 제공자를 통해 읽거나 쓸 수 있음, 이를 변경하려면 매니페스트 파일에서 <provider> 요소의 자식 요소나 속성을 사용해 제공자를 위한 권한을 설정해야 함, 권한을 특정 레코드, 특정 테이블, 전체 제공자 또는 세 가지 모두에 적용할 수 있음

  • 매니페스트 파일에서 하나 이상의 <permission> 요소를 사용해 제공자에 권한을 정의할 수 있음, 권한을 제공자에 고유하게 만들고 싶다면 android:name 속성에 자바 속성의 범위를 사용해야 함, 예시로 읽기 권한을 com.example.app.provider.permission.READ_PROVIDER 로 이름 붙일 수 있음

  • 다음 리스트는 제공자 권한의 범위를 설명하고 전체 제공자에 적용되는 권한부터 설명하고 뒤로 갈수록 세부적인 권한임, 세부 권한은 더 큰 범위의 권한보다 우선적으로 적용됨

제공자 수준의 단일 읽기 쓰기 권한

  • 전체 제공자에 대해 읽기와 쓰기 접근을 제어하는 하나의 권한이고, <provider> 요소의 android:permission 속성으로 지정함

제공자 수준의 별도의 읽기 쓰기 권한

  • 전체 제공자에 대한 읽기 권한과 쓰기 권한이고, <provider> 요소의 android:readPermissionandroid:writePermission 속성으로 지정함, 이는 android:permission 에서 요구하는 권한보다 우선됨

경로 수준의 권한

  • 제공자의 컨텐츠 URI에 대한 읽기, 쓰기 또는 읽기 및 쓰기 권한이고, <provider> 요소의 자식 요소인 <path-permission> 로 제어할 URI를 지정할 수 있음, 지정하는 URI 마다 읽기 및 쓰기 권한, 읽기 권한, 쓰기 권한 또는 세 가지 모두 지정할 수 있음, 읽기 권한과 쓰기 권한은 읽기 및 쓰기 권한보다 우선으로 적용되며 또한 경로 수준의 권한은 제공자 수준의 권한보다 우선으로 적용됨

임시 권한

  • 앱이 일반적으로 요구되는 권한을 가지고 있지 않더라도 임시적으로 접근을 부여하기 위한 권한 수준임, 임시 접근 기능은 앱이 매니페스트에서 요청해야하는 권한의 수를 줄여줌, 임시 권한을 사용하면 영구 권한이 필요한 앱은 제공자의 모든 데이터에 대해 지속적으로 접근해야 하는 앱만 남음

  • 예시로 이메일 제공자와 앱을 구현하고 있는 상황에서 외부 이미지 뷰어 앱이 사진 첨부파일을 표시하기 위해 제공자에서 가져올 때의 권한을 고려한다면, 이미지 뷰어가 별도의 권한 요청 없이 접근 할 수 있도록 사진의 컨텐츠 URI에 대한 임시 권한을 설정할 수 있음

  • 그 후, 이메일 앱을 설계할 때 사용자가 사진 표시를 원하면 앱은 이미지 뷰어에게 사진의 컨텐츠 URI와 권한 플래그를 포함하는 인텐트를 전송함, 그러면 이미지 뷰어는 제공자에 대한 일반 읽기 권한이 없더라도 이메일 제공자에서 사진을 검색할 수 있음

  • 임시 권한을 사용하려면 <provider> 요소의 android:grantUriPermissions 속성을 설정하거나 <provider> 요소에 하나 이상의 <grant-uri-permission> 자식 속성을 추가해야 함, 컨텐츠 URI에 대한 임시 권한을 제거해야 할 때 Context.revokeUriPermission() 를 반드시 호출해야 함

  • android:grantUriPermissions 속성은 제공자가 얼마나 접근이 가능한지를 결정함, 이 속성이 "true" 로 설정된다면 시스템이 필요로 하는 제공자 수준 또는 경로 수준의 권한을 덮어씌워 임시 권한이 전체 제공자를 이용할 수 있도록 부여함, 속성이 "false" 로 설정된다면 임시 권한은 <provider> 요소의 하위 요소인 <grant-uri-permission> 으로 지정된 컨텐츠 URI에 대해서만 접근이 가능함

  • 앱에 임시 권한을 부여하려면 인텐트는 반드시 FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION 또는 두 플래그 모두를 포함해야 함, setFlags() 메소드를 사용해 플래그를 설정할 수 있음

  • android:grantUriPermissions 속성이 제공되지 않는다면 "false" 로 설정되어 있는 것으로 간주함

<provider> 요소

  • 액티비티나 서비스 컴포넌트처럼 ContentProvider 의 자식 클래스는 <provider> 요소를 사용해 앱의 매니페스트 파일에 정의됨, 안드로이드 시스템은 요소에서 다음과 같은 정보를 가져옴

Authority (android:authorities)

  • 시스템이 전체 제공자를 인식하기 위한 식별자

제공자 클래스 이름 (android:name)

  • ContentProvider 를 구현한 클래스의 이름

권한

  • 다른 앱이 제공자의 데이터에 접근하기 위해 반드시 가져야 하는 권한을 지정하는 속성
    • android:grantUriPermissions : 임시 권한 플래그
    • android:permission : 제공자 수준의 단일 읽기 및 쓰기 권한
    • android:readPermission : 제공자 수준의 읽기 권한
    • android:writePermission : 제공자 수준의 쓰기 권한

시작 및 제어 속성

  • 안드로이드 시스템이 제공자를 언제 어떻게 시작할지, 제공자의 프로세스 특성 또는 기타 런타임 설정들을 결정하는 속성
    • android:enabled : 시스템이 제공자를 시작하도록 허용하는 플래그
    • android:exported : 다른 앱이 제공자를 사용하도록 허용하는 플래그
    • android:initOrder : 같은 프로세스 내에 있는 다른 제공자들과 비교하여, 이 제공자가 시작되는 순서
    • android:multiProcess : 시스템이 제공자를 호출한 클라이언트와 동일한 프로세스에서 시작하도록 허용하는 플래그
    • android:process : 제공자가 실행되는 프로세스의 이름
    • android:syncable : 제공자의 데이터가 서버의 데이터와 동기화되는지 나타내는 플래그

정보 속성

  • android:icon : 제공자의 아이콘을 포함하는 드로어블 리소스, 앱 리스트에서 제공자의 이름 옆에 표시되는 아이콘

  • android:label : 제공자, 데이터 또는 둘 다 설명하는 정보성 이름, 앱 리스트에서 표시되는 이름

  • 이런 속성들은 <provider> 요소에 문서화되어 있음

노트: 만약 안드로이드 11 이상을 타겟팅한다면, 추가 구성 요구사항을 위해 패키지 가시성 문서를 확인하세요.

인텐트와 데이터 접근

  • 앱은 인텐트를 사용해 간접적으로 컨텐츠 제공자에 접근할 수 있음, 앱이 ContentResolverContentProvider 의 메소드를 호출하는 대신 제공자의 앱에 일부인 액티비티에 인텐트를 전송해 시작함, 목적지 액티비티는 UI에서 데이터를 검색하거나 표시하는 역할을 함

  • 인텐트의 작업에 따라 목적지 액티비티는 사용자에게 제공자의 데이터를 수정하도록 안내할 수 있음, 인텐트에 extras 데이터가 포함될 수 있으며 이 데이터는 목적지 액티비티의 UI에 표시됨, 사용자는 이 데이터를 제공자 데이터를 수정하는 데 사용하기 전에 값을 변경할 수 있게 됨

  • 데이터 무결성을 지키기 위해 위와 같이 인텐트로 작업하는 방식을 사용할 수 있음, 제공자의 데이터 삽입, 수정, 삭제가 엄격하게 정의된 비즈니스 로직에 의존한다면 다른 앱이 데이터를 직접 수정하는 것은 유효하지 않은 데이터를 만들 수 있기 때문에 위의 방식이 도움이 됨

  • 다른 개발자가 인텐트를 사용해 접근하게 하려면, 코드에서 직접 데이터를 수정하기보다 인텐트를 사용해 제공자가 있는 앱의 UI를 사용하는 게 왜 더 좋은지 설명해야 하고 어떻게 사용하는지 철저히 문서화해야 함

  • 제공자의 데이터를 수정하려고 전달되는 인텐트를 처리하는 것은 다른 인텐트를 처리하는 것과 별로 다를 것이 없음, 인텐트를 사용하는 것에 대해 더 배우고 싶다면 인텐트와 인텐트 필터를 읽어보면 좋음

원문: https://developer.android.com/guide/topics/providers/content-provider-creating

0개의 댓글