콘텐츠 제공자 만들기

콘텐츠 제공자는 중앙 저장소로의 데이터 액세스를 관리합니다. Android 애플리케이션에서 제공자는 매니페스트 파일의 요소와 함께 하나 이상의 클래스로 구현합니다. 클래스 중 하나가 제공자와 다른 애플리케이션 간의 인터페이스인 ContentProvider의 서브클래스를 구현합니다.

콘텐츠 제공자는 다른 애플리케이션에서 데이터를 사용할 수 있게 해야 하지만, 개발자는 사용자가 제공자가 관리하는 데이터를 쿼리하고 수정하도록 허용하는 활동을 애플리케이션에 포함할 수 있습니다.

이 페이지에는 콘텐츠 제공자를 빌드하는 기본 프로세스와 사용할 API 목록이 포함되어 있습니다.

빌드를 시작하기 전에

제공자를 빌드하기 전에 우선 다음 단계를 고려하세요.

  • 콘텐츠 제공자가 필요한지 결정합니다. 다음 기능 중 하나 이상을 제공하려면 콘텐츠 제공자를 빌드해야 합니다.
    • 다른 애플리케이션에 복잡한 데이터나 파일을 제공하고자 하는 경우
    • 사용자가 복잡한 데이터를 내 앱에서 다른 앱으로 복사할 수 있도록 하려고 합니다.
    • 검색 프레임워크를 사용한 사용자 지정 검색 제안을 제공하고자 하는 경우
    • 애플리케이션 데이터를 위젯에 노출하고자 하는 경우
    • AbstractThreadedSyncAdapter, CursorAdapter 또는 CursorLoader 클래스를 구현하려고 합니다.

    전적으로 자체 애플리케이션 내에서 사용하며 위에 나열된 기능이 필요하지 않은 경우에는 제공업체가 데이터베이스나 다른 유형의 영구 저장소를 사용할 필요가 없습니다. 대신 데이터 및 파일 저장소 개요에 설명된 저장소 시스템 중 하나를 사용할 수 있습니다.

  • 아직 읽지 않았다면 콘텐츠 제공자 기본사항을 읽고 제공자와 제공자의 작동 방식에 관해 자세히 알아보세요.

그런 다음, 다음 단계를 따라 제공자를 빌드하세요.

  1. 데이터를 위한 원시 저장소를 설계합니다. 콘텐츠 제공자는 두 가지 방법으로 데이터를 제공합니다.
    파일 데이터
    일반적으로 사진, 오디오, 동영상과 같은 파일에 들어가는 데이터입니다. 애플리케이션의 전용 공간에 파일을 저장합니다. 제공자는 다른 애플리케이션의 파일 요청에 응답하여 파일의 핸들을 제공할 수 있습니다.
    '구조화된' 데이터
    일반적으로 데이터베이스, 배열 또는 유사한 구조로 들어가는 데이터입니다. 행 및 열 테이블과 호환되는 형식으로 데이터를 저장합니다. 행은 사람이나 인벤토리의 항목과 같은 항목을 나타냅니다. 열은 개인의 이름이나 상품 가격과 같은 항목의 일부 데이터를 나타냅니다. 이러한 유형의 데이터를 저장하는 일반적인 방법은 SQLite 데이터베이스에 저장하는 것이지만, 모든 유형의 영구 저장소를 사용할 수 있습니다. Android 시스템에서 사용할 수 있는 저장소 유형에 관해 자세히 알아보려면 데이터 저장소 설계 섹션을 참고하세요.
  2. ContentProvider 클래스와 필수 메서드의 구체적인 구현을 정의합니다. 이 클래스는 데이터와 나머지 Android 시스템 간의 인터페이스입니다. 이 클래스에 관한 자세한 내용은 ContentProvider 클래스 구현 섹션을 참조하세요.
  3. 제공자의 권한 문자열, 콘텐츠 URI 및 열 이름을 정의합니다. 제공자의 애플리케이션이 인텐트를 처리하도록 하려면 인텐트 작업, 추가 데이터 및 플래그도 정의합니다. 데이터에 액세스하려는 애플리케이션에 필요한 권한도 정의합니다. 이러한 모든 값을 별도의 계약 클래스에서 상수로 정의해 보세요. 나중에 이 클래스를 다른 개발자에게 노출할 수 있습니다. 콘텐츠 URI에 관한 자세한 내용은 콘텐츠 URI 설계 섹션을 참고하세요. 인텐트에 관한 자세한 내용은 인텐트 및 데이터 액세스 섹션을 참고하세요.
  4. 샘플 데이터 또는 제공자와 클라우드 기반 데이터 간에 데이터를 동기화할 수 있는 AbstractThreadedSyncAdapter 구현과 같은 다른 선택적 부분을 추가합니다.

데이터 저장소 설계

콘텐츠 제공자는 구조화된 형식으로 저장된 데이터로 연결되는 인터페이스입니다. 인터페이스를 만들기 전에 데이터 저장 방법을 결정합니다. 데이터를 원하는 형식으로 저장한 다음 필요에 따라 데이터를 읽고 쓰도록 인터페이스를 설계합니다.

다음은 Android에서 사용할 수 있는 몇 가지 데이터 저장소 기술입니다.

  • 구조화된 데이터로 작업하는 경우 SQLite와 같은 관계형 데이터베이스 또는 LevelDB와 같은 비관계형 키-값 데이터 스토어를 사용하는 것이 좋습니다. 오디오, 이미지 또는 동영상 미디어와 같은 구조화되지 않은 데이터로 작업하는 경우 데이터를 파일로 저장하는 것이 좋습니다. 여러 유형의 저장소를 혼합하여 사용하고 필요한 경우 단일 콘텐츠 제공자를 사용하여 노출할 수 있습니다.
  • Android 시스템은 Room 지속성 라이브러리와 상호작용할 수 있습니다. Room 지속성 라이브러리는 Android의 자체 제공자가 테이블 지향적 데이터를 저장하는 데 사용하는 SQLite 데이터베이스 API에 대한 액세스 권한을 제공합니다. 이 라이브러리를 사용하여 데이터베이스를 만들려면 Room을 사용하여 로컬 데이터베이스에 데이터 저장에 설명된 대로 RoomDatabase의 서브클래스를 인스턴스화합니다.

    저장소를 구현할 때 데이터베이스를 사용할 필요가 없습니다. 제공자는 외부에 테이블 집합으로서 관계형 데이터베이스와 유사하게 표시되지만 이것이 제공자의 내부 구현에 필요한 것은 아닙니다.

  • 파일 데이터 저장의 경우, Android에는 다양한 파일 지향적 API가 있습니다. 파일 저장소에 관한 자세한 내용은 데이터 및 파일 저장소 개요를 참고하세요. 음악이나 동영상 같은 미디어 관련 데이터를 제공하는 제공자를 설계하는 경우, 제공자가 테이블 데이터와 파일을 조합할 수 있습니다.
  • 드문 경우지만 단일 애플리케이션에 콘텐츠 제공자를 두 개 이상 구현하면 도움이 될 수 있습니다. 예를 들어 하나의 콘텐츠 제공자를 사용하여 위젯과 일부 데이터를 공유하고 다른 애플리케이션과 공유하기 위해 다른 데이터 세트를 노출해야 할 수 있습니다.
  • 네트워크 기반 데이터로 작업하려면 java.netandroid.net의 클래스를 사용합니다. 네트워크 기반 데이터를 데이터베이스와 같은 로컬 데이터 저장소와 동기화한 다음 해당 데이터를 테이블이나 파일로 제공할 수도 있습니다.

참고: 이전 버전과 호환되지 않는 저장소 변경사항을 적용하는 경우 저장소를 새 버전 번호로 표시해야 합니다. 새로운 콘텐츠 제공자를 구현하는 앱의 버전 번호도 높여야 합니다. 이렇게 변경하면 호환되지 않는 콘텐츠 제공자가 있는 앱을 재설치하려고 할 때 시스템 다운그레이드로 인해 시스템이 비정상 종료되는 것을 방지합니다.

데이터 설계 시 고려할 사항

다음은 제공자의 데이터 구조를 설계하기 위한 몇 가지 팁입니다.

  • 테이블 데이터에는 항상 제공자가 각 행의 고유한 숫자 값으로 유지하는 '기본 키' 열이 있어야 합니다. 이 값을 사용하여 행을 다른 테이블의 관련 행에 연결할 수 있습니다('외래 키'로 사용). 이 열에는 어떤 이름이든 사용할 수 있지만 제공자 쿼리 결과를 ListView에 연결하려면 검색된 열 중 하나의 이름이 _ID여야 하므로 BaseColumns._ID를 사용하는 것이 가장 좋습니다.
  • 비트맵 이미지나 기타 매우 큰 파일 지향 데이터 조각을 제공하려면 테이블에 직접 저장하는 대신 파일에 데이터를 저장한 다음 간접적으로 제공합니다. 이렇게 하는 경우 제공자의 사용자에게 데이터에 액세스하려면 ContentResolver 파일 메서드를 사용해야 한다고 알려야 합니다.
  • 바이너리 대형 객체 (BLOB) 데이터 유형을 사용하여 크기가 다르거나 구조가 다양한 데이터를 저장합니다. 예를 들어 BLOB 열을 사용하여 프로토콜 버퍼JSON 구조를 저장할 수 있습니다.

    BLOB를 사용하여 스키마에 종속되지 않은 테이블을 구현할 수도 있습니다. 이 유형의 테이블에서는 기본 키 열, MIME 유형 열, 하나 이상의 일반적인 열을 BLOB로 정의합니다. BLOB 열에 있는 데이터의 의미는 MIME 유형 열의 값으로 나타냅니다. 이렇게 하면 동일한 테이블에 여러 행 유형을 저장할 수 있습니다. 연락처 제공자의 '데이터' 테이블 ContactsContract.Data는 스키마 독립형 테이블의 예입니다.

콘텐츠 URI 설계

콘텐츠 URI는 제공자에서 데이터를 식별하는 URI입니다. 콘텐츠 URI에는 전체 제공자의 상징적인 이름 (제공자의 권한)과 테이블이나 파일을 가리키는 이름 (경로)이 포함됩니다. ID 부분(선택사항)은 테이블의 개별 행을 가리킵니다. ContentProvider의 모든 데이터 액세스 메서드에는 콘텐츠 URI가 인수로 있습니다. 이를 통해 액세스할 테이블, 행 또는 파일을 결정할 수 있습니다.

콘텐츠 URI에 관한 자세한 내용은 콘텐츠 제공자 기본사항을 참고하세요.

권한 설계

제공자에는 보통 하나의 권한이 있으며, 이것이 Android 내부 이름 역할을 합니다. 다른 제공업체와의 충돌을 방지하려면 인터넷 도메인 소유권 (역방향)을 제공업체 권한의 기반으로 사용하세요. 이 권장사항은 Android 패키지 이름에도 적용되므로 제공자 권한을 제공자가 포함된 패키지 이름의 확장으로 정의할 수 있습니다.

예를 들어 Android 패키지 이름이 com.example.<appname>이면 제공자에 com.example.<appname>.provider 권한을 부여하세요.

경로 구조 설계

개발자는 일반적으로 권한으로부터 콘텐츠 URI를 생성할 때 개별 테이블을 가리키는 경로를 추가합니다. 예를 들어 table1table2라는 두 테이블이 있다면 두 테이블을 이전 예의 권한과 결합하여 콘텐츠 URI com.example.<appname>.provider/table1com.example.<appname>.provider/table2를 생성할 수 있습니다. 경로는 단일 세그먼트로 제한되지 않으며 경로의 각 수준에 대한 테이블이 필요하지 않습니다.

콘텐츠 URI ID 처리

규칙에 따라 제공자는 URI 끝에 행의 ID 값이 있는 콘텐츠 URI를 허용하여 테이블의 단일 행에 관한 액세스를 제공합니다. 또한 제공자는 규칙에 따라 ID 값을 테이블의 _ID 열과 일치시키고 일치하는 행에 대해 요청된 액세스를 수행합니다.

이 규칙은 제공자에 액세스하는 앱을 위한 공통 설계 패턴을 세우는 데 유용합니다. 앱은 제공자를 대상으로 쿼리를 실행하고 결과 CursorCursorAdapter를 사용하여 ListView에 표시합니다. CursorAdapter의 정의에 따르면 Cursor의 열 중 하나가 _ID이어야 합니다.

그런 다음 사용자는 데이터를 보거나 수정하기 위해 UI에서 표시된 행 중 하나를 선택합니다. 앱은 ListView를 지원하는 Cursor에서 상응하는 행을 가져와 이 행의 _ID 값을 가져와 콘텐츠 URI에 추가하고 제공자에 액세스 요청을 보냅니다. 그러면 제공자는 사용자가 선택한 정확한 행에 대해 쿼리하거나 수정할 수 있습니다.

콘텐츠 URI 패턴

수신되는 콘텐츠 URI에 실행할 작업을 선택할 수 있도록 제공업체 API에는 콘텐츠 URI 패턴을 정수 값에 매핑하는 편의 클래스 UriMatcher가 포함되어 있습니다. 특정 패턴과 일치하는 하나 또는 여러 개의 콘텐츠 URI에 대해 원하는 작업을 선택하는 switch 문에서 정수 값을 사용할 수 있습니다.

콘텐츠 URI 패턴은 와일드카드 문자를 사용하는 콘텐츠 URI와 일치합니다.

  • *는 길이에 관계없이 유효한 모든 문자로 구성된 문자열과 일치합니다.
  • #는 모든 길이의 숫자 문자로 구성된 문자열과 일치합니다.

콘텐츠 URI 처리를 설계하고 코딩하는 예로, 테이블을 가리키는 다음 콘텐츠 URI를 인식하는 권한 com.example.app.provider를 가진 제공자가 있다고 가정해 보겠습니다.

  • 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라는 테이블입니다.

제공자는 행 ID가 추가된 경우 이러한 콘텐츠 URI도 인식합니다(예: table3에서 1로 식별되는 행의 경우 content://com.example.app.provider/table3/1).

다음과 같은 콘텐츠 URI 패턴이 있을 수 있습니다.

content://com.example.app.provider/*
제공자의 모든 콘텐츠 URI와 일치합니다.
content://com.example.app.provider/table2/*
dataset1dataset2 테이블의 콘텐츠 URI와 일치하지만 table1 또는 table3의 콘텐츠 URI와 일치하지 않습니다.
content://com.example.app.provider/table3/#
table3의 단일 행에 대한 콘텐츠 URI와 일치합니다(예: 6로 식별된 행의 content://com.example.app.provider/table3/6).

다음 코드 스니펫은 UriMatcher의 메서드가 작동하는 방식을 보여줍니다. 이 코드는 테이블에 콘텐츠 URI 패턴 content://<authority>/<path>을 사용하고 단일 행에는 content://<authority>/<path>/<id>을 사용하여 단일 행의 URI와는 다르게 전체 테이블의 URI를 처리합니다.

addURI() 메서드는 권한과 경로를 정수 값에 매핑합니다. match() 메서드는 URI의 정수 값을 반환합니다. switch 문은 전체 테이블을 쿼리할 것인지, 단일 레코드를 쿼리할 것인지 선택합니다.

Kotlin

private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    /*
     * The calls to addURI() go here for all the content URI patterns that the provider
     * recognizes. For this snippet, only the calls for table 3 are shown.
     */

    /*
     * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
     * in the path.
     */
    addURI("com.example.app.provider", "table3", 1)

    /*
     * Sets the code for a single row to 2. In this case, the # wildcard is
     * used. content://com.example.app.provider/table3/3 matches, but
     * content://com.example.app.provider/table3 doesn't.
     */
    addURI("com.example.app.provider", "table3/#", 2)
}
...
class ExampleProvider : ContentProvider() {
    ...
    // Implements 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 -> { // If the incoming URI was for all of table3
                if (localSortOrder.isEmpty()) {
                    localSortOrder = "_ID ASC"
                }
            }
            2 -> {  // If the incoming URI was for a single row
                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query.
                 */
                localSelection += "_ID ${uri?.lastPathSegment}"
            }
            else -> { // If the URI isn't recognized,
                // do some error handling here
            }
        }

        // Call the code to actually do the query
    }
}

Java

public class ExampleProvider extends ContentProvider {
...
    // Creates a UriMatcher object.
    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        /*
         * The calls to addURI() go here for all the content URI patterns that the provider
         * recognizes. For this snippet, only the calls for table 3 are shown.
         */

        /*
         * Sets the integer value for multiple rows in table 3 to one. No wildcard is used
         * in the path.
         */
        uriMatcher.addURI("com.example.app.provider", "table3", 1);

        /*
         * Sets the code for a single row to 2. In this case, the # wildcard is
         * used. content://com.example.app.provider/table3/3 matches, but
         * content://com.example.app.provider/table3 doesn't.
         */
        uriMatcher.addURI("com.example.app.provider", "table3/#", 2);
    }
...
    // Implements ContentProvider.query()
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
...
        /*
         * Choose the table to query and a sort order based on the code returned for the incoming
         * URI. Here, too, only the statements for table 3 are shown.
         */
        switch (uriMatcher.match(uri)) {


            // If the incoming URI was for all of table3
            case 1:

                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;

            // If the incoming URI was for a single row
            case 2:

                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query.
                 */
                selection = selection + "_ID = " + uri.getLastPathSegment();
                break;

            default:
            ...
                // If the URI isn't recognized, do some error handling here
        }
        // Call the code to actually do the query
    }

또 다른 클래스인 ContentUris는 콘텐츠 URI의 id 부분을 사용하기 위한 편의 메서드를 제공합니다. UriUri.Builder 클래스에는 기존 Uri 객체를 파싱하고 새 객체를 빌드하기 위한 편의 메서드가 포함되어 있습니다.

ContentProvider 클래스 구현

ContentProvider 인스턴스는 다른 애플리케이션의 요청을 처리하여 구조화된 데이터 세트에 대한 액세스를 관리합니다. 모든 형태의 액세스는 결국 ContentResolver를 호출하고 이는 액세스 권한을 얻기 위해 구체적인 ContentProvider 메서드를 호출합니다.

필수 메서드

추상 클래스 ContentProvider는 구체적인 서브클래스의 일부로 구현하는 6개의 추상 메서드를 정의합니다. onCreate()를 제외한 이러한 모든 메서드는 콘텐츠 제공자에 액세스하려고 하는 클라이언트 애플리케이션에서 호출합니다.

query()
제공업체에서 데이터를 검색합니다. 인수를 사용하여 쿼리할 테이블과 반환할 행과 열, 결과의 정렬 순서를 선택합니다. 데이터를 Cursor 객체로 반환합니다.
insert()
제공업체에 새 행을 삽입합니다. 인수를 사용하여 대상 테이블을 선택하고 사용할 열 값을 가져옵니다. 새로 삽입된 행의 콘텐츠 URI를 반환합니다.
update()
제공업체의 기존 행을 업데이트합니다. 인수를 사용하여 업데이트할 테이블과 행을 선택하고 업데이트된 열 값을 가져옵니다. 업데이트한 행 개수를 반환합니다.
delete()
제공업체에서 제공하는 행을 삭제합니다. 인수를 사용하여 삭제할 테이블과 행을 선택합니다. 삭제한 행 개수를 반환합니다.
getType()
콘텐츠 URI에 상응하는 MIME 유형을 반환합니다. 이 메서드는 콘텐츠 제공자 MIME 유형 구현 섹션에 자세히 설명되어 있습니다.
onCreate()
제공업체를 초기화합니다. Android 시스템에서는 제공자를 생성한 직후에 이 메서드를 호출합니다. ContentResolver 객체가 제공자에 액세스하려고 시도할 때까지 제공자가 생성되지 않습니다.

이러한 메서드에는 동일하게 이름이 지정된 ContentResolver 메서드와 서명이 동일합니다.

이러한 메서드를 구현할 때는 다음 사항을 고려해야 합니다.

  • onCreate()를 제외한 이러한 모든 메서드는 한 번에 여러 스레드에서 호출할 수 있으므로 스레드로부터 안전해야 합니다. 여러 스레드에 관한 자세한 내용은 프로세스 및 스레드 개요를 참고하세요.
  • onCreate()에서 긴 작업을 실행하지 마세요. 실제로 필요할 때까지 초기화 작업을 미뤄두세요. onCreate() 메서드 구현 섹션에서 자세히 설명합니다.
  • 이러한 메서드는 반드시 구현해야 하지만, 코드는 예상 데이터 유형을 반환하는 것 외에는 아무것도 할 필요가 없습니다. 예를 들어 insert() 호출을 무시하고 0을 반환하여 다른 애플리케이션이 일부 테이블에 데이터를 삽입하지 못하게 할 수 있습니다.

query() 메서드 구현

ContentProvider.query() 메서드는 Cursor 객체를 반환하거나 실패하면 Exception을 발생시킵니다. SQLite 데이터베이스를 데이터 저장소로 사용하는 경우 SQLiteDatabase 클래스의 query() 메서드 중 하나에서 반환된 Cursor를 반환할 수 있습니다.

쿼리가 어떤 행과도 일치하지 않으면 getCount() 메서드가 0을 반환하는 Cursor 인스턴스를 반환합니다. 쿼리 프로세스 중에 내부 오류가 발생한 경우에만 null를 반환합니다.

SQLite 데이터베이스를 데이터 저장소로 사용하지 않는다면 Cursor의 구체적인 서브클래스 중 하나를 사용하세요. 예를 들어 MatrixCursor 클래스는 각 행이 Object 인스턴스의 배열인 커서를 구현합니다. 이 클래스에서 addRow()를 사용하여 새 행을 추가합니다.

Android 시스템은 프로세스 경계를 넘어 Exception를 전달할 수 있어야 합니다. Android에서는 쿼리 오류를 처리하는 데 유용한 다음과 같은 예외의 경우에 이 작업을 실행할 수 있습니다.

insert() 메서드 구현

insert() 메서드는 ContentValues 인수의 값을 사용하여 적절한 테이블에 새 행을 추가합니다. 열 이름이 ContentValues 인수에 없으면 제공업체 코드나 데이터베이스 스키마에 기본값을 제공하는 것이 좋습니다.

이 메서드는 새 행의 콘텐츠 URI를 반환합니다. 이를 구성하려면 withAppendedId()를 사용하여 새 행의 기본 키(대개 _ID 값)를 테이블의 콘텐츠 URI에 추가합니다.

delete() 메서드 구현

delete() 메서드를 사용하면 데이터 저장소에서 행을 삭제할 필요가 없습니다. 제공자와 동기화 어댑터를 함께 사용하는 경우, 행을 완전히 삭제하는 대신 삭제된 행을 '삭제' 플래그로 표시하는 것이 좋습니다. 동기화 어댑터는 삭제된 행을 확인하고, 이를 제공자에서 삭제하기 전에 서버에서 제거할 수 있습니다.

update() 메서드 구현

update() 메서드는 insert()에서 사용하는 것과 동일한 ContentValues 인수, 그리고 delete()ContentProvider.query()에서 사용하는 동일한 selectionselectionArgs 인수를 사용합니다. 이렇게 하면 이러한 메서드 간에 코드를 재사용할 수 있습니다.

onCreate() 메서드 구현

Android 시스템은 제공자를 시작할 때 onCreate()를 호출합니다. 이 메서드에서는 빠르게 실행되는 초기화 작업만 실행하고 제공자가 실제로 데이터 요청을 수신할 때까지 데이터베이스 생성과 데이터 로드를 지연시킵니다. onCreate()에서 긴 작업을 실행하면 제공업체의 시작 속도가 느려집니다. 이로 인해 제공자에서 다른 애플리케이션으로의 응답 속도가 느려집니다.

다음 두 스니펫은 ContentProvider.onCreate() Room.databaseBuilder() 간의 상호작용을 보여줍니다. 첫 번째 스니펫은 데이터베이스 객체가 빌드되고 데이터 액세스 객체에 대한 핸들이 생성된 ContentProvider.onCreate()의 구현을 보여줍니다.

Kotlin

// Defines the database name
private const val DBNAME = "mydb"
...
class ExampleProvider : ContentProvider() {

    // Defines a handle to the Room database
    private lateinit var appDatabase: AppDatabase

    // Defines a Data Access Object to perform the database operations
    private var userDao: UserDao? = null

    override fun onCreate(): Boolean {

        // Creates a new database object
        appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, DBNAME).build()

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.userDao

        return true
    }
    ...
    // Implements the provider's insert method
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc.
    }
}

Java

public class ExampleProvider extends ContentProvider

    // Defines a handle to the Room database
    private AppDatabase appDatabase;

    // Defines a Data Access Object to perform the database operations
    private UserDao userDao;

    // Defines the database name
    private static final String DBNAME = "mydb";

    public boolean onCreate() {

        // Creates a new database object
        appDatabase = Room.databaseBuilder(getContext(), AppDatabase.class, DBNAME).build();

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.getUserDao();

        return true;
    }
    ...
    // Implements the provider's insert method
    public Cursor insert(Uri uri, ContentValues values) {
        // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc.
    }
}

ContentProvider MIME 유형 구현

ContentProvider 클래스에는 MIME 유형을 반환하는 두 가지 메서드가 있습니다.

getType()
모든 제공자에 구현하는 필수 메서드 중 하나입니다.
getStreamTypes()
제공업체가 파일을 제공하는 경우 구현해야 하는 메서드입니다.

테이블의 MIME 유형

getType() 메서드는 콘텐츠 URI 인수에서 반환된 데이터 유형을 설명하는 MIME 형식의 String를 반환합니다. Uri 인수는 특정 URI가 아닌 패턴일 수 있습니다. 이 경우 패턴과 일치하는 콘텐츠 URI와 연결된 데이터 유형을 반환합니다.

텍스트, HTML, JPEG와 같은 일반적인 데이터 유형의 경우 getType()는 데이터의 표준 MIME 유형을 반환합니다. 이러한 표준 유형의 전체 목록은 IANA MIME 미디어 유형 웹사이트에서 확인할 수 있습니다.

테이블 데이터의 행 또는 행을 가리키는 콘텐츠 URI의 경우 getType()는 Android의 공급업체별 MIME 형식으로 MIME 유형을 반환합니다.

  • 유형 부분: vnd
  • 하위유형 부분:
    • URI 패턴이 단일 행에 관한 경우: android.cursor.item/
    • URI 패턴이 두 개 이상의 행에 적용되는 경우: android.cursor.dir/
  • 제공자별 부분: vnd.<name><type>

    개발자가 <name><type>를 제공합니다. <name> 값은 전역적으로 고유하며, <type> 값은 상응하는 URI 패턴마다 고유합니다. <name>로는 회사 이름 또는 애플리케이션의 Android 패키지 이름의 일부를 사용하는 것이 좋습니다. <type>에는 URI와 연결된 테이블을 식별하는 문자열을 사용하는 것이 좋습니다.

예를 들어 제공자의 권한이 com.example.app.provider이고 table1라는 테이블을 노출하는 경우 table1의 여러 행에 관한 MIME 유형은 다음과 같습니다.

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

table1의 단일 행의 경우 MIME 유형은 다음과 같습니다.

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

파일의 MIME 유형

제공업체에서 파일을 제공하는 경우 getStreamTypes()를 구현합니다. 이 메서드는 제공자가 지정된 콘텐츠 URI에 관해 반환할 수 있는 파일의 MIME 유형의 String 배열을 반환합니다. 제공하는 MIME 유형을 MIME 유형 필터 인수로 필터링하여 클라이언트가 처리하려는 MIME 유형만 반환하도록 합니다.

예를 들어 사진 이미지를 JPG, PNG, GIF 형식의 파일로 제공하는 제공자가 있다고 가정해 보겠습니다. 애플리케이션이 '이미지'에 대해 필터 문자열 image/*ContentResolver.getStreamTypes()를 호출하면 ContentProvider.getStreamTypes() 메서드가 배열을 반환합니다.

{ "image/jpeg", "image/png", "image/gif"}

앱이 JPG 파일만 필요한 경우 필터 문자열 *\/jpegContentResolver.getStreamTypes()를 호출할 수 있으며 getStreamTypes()는 다음을 반환합니다.

{"image/jpeg"}

제공업체가 필터 문자열에 요청된 MIME 유형 중 어떤 것도 제공하지 않으면 getStreamTypes()null를 반환합니다.

계약 클래스 구현

계약 클래스는 URI, 열 이름, MIME 유형 및 제공자와 관련된 기타 메타데이터의 상수 정의를 포함하는 public final 클래스입니다. 이 클래스는 URI, 열 이름 등의 실제 값이 변경된 경우에도 제공자에 올바르게 액세스할 수 있도록 하여 제공업체와 다른 애플리케이션 간의 계약을 설정합니다.

계약 클래스는 개발자에게도 도움이 됩니다. 일반적으로 상수로 니모닉 이름을 가지기 때문에 개발자가 열 이름 또는 URI에 잘못된 값을 사용할 가능성이 작습니다. 클래스이므로 Javadoc 문서를 포함할 수 있습니다. Android 스튜디오와 같은 통합 개발 환경은 계약 클래스의 상수 이름을 자동 완성하고 상수의 Javadoc을 표시할 수 있습니다.

개발자는 애플리케이션에서 계약 클래스의 클래스 파일에 액세스할 수 없지만 개발자가 제공하는 JAR 파일에서 이를 애플리케이션으로 정적으로 컴파일할 수 있습니다.

ContactsContract 클래스와 중첩된 클래스는 계약 클래스의 예입니다.

콘텐츠 제공자 권한 구현

Android 시스템의 모든 측면에 관한 권한 및 액세스는 보안 도움말에 자세히 설명되어 있습니다. 또한 데이터 및 파일 저장소 개요에서는 다양한 유형의 저장소에 적용되는 보안 및 권한을 설명합니다. 간단히 말해, 중요한 사항은 다음과 같습니다.

  • 기본적으로 기기의 내부 저장소에 저장된 데이터 파일은 애플리케이션과 제공자 전용입니다.
  • 생성한 SQLiteDatabase 데이터베이스는 애플리케이션과 제공업체에 비공개됩니다.
  • 기본적으로 외부 저장소에 저장하는 데이터 파일은 공개이며 누구나 읽을 수 있습니다. 다른 애플리케이션이 다른 API 호출을 사용하여 파일을 읽고 쓸 수 있으므로 콘텐츠 제공자를 사용하여 외부 저장소의 파일에 대한 액세스를 제한할 수 없습니다.
  • 기기의 내부 저장소에서 파일 또는 SQLite 데이터베이스를 열거나 생성하기 위한 메서드 호출은 다른 모든 애플리케이션에 읽기 및 쓰기 액세스 권한을 부여할 가능성이 있습니다. 내부 파일이나 데이터베이스를 제공자의 저장소로 사용하고 '누구나 읽을 수 있는' 또는 '누구나 쓸 수 있는' 액세스 권한을 부여하는 경우 매니페스트에서 제공자에 설정한 권한은 데이터를 보호하지 않습니다. 내부 저장소에 있는 파일과 데이터베이스의 기본 액세스는 '비공개'입니다. 제공자의 저장소용으로 변경하면 안 됩니다.

콘텐츠 제공자 권한을 사용하여 데이터에 대한 액세스를 제어하려면 내부 파일, SQLite 데이터베이스 또는 클라우드(예: 원격 서버)에 데이터를 저장하고 파일과 데이터베이스를 애플리케이션에 비공개로 유지합니다.

권한 구현

기본적으로 제공자에는 권한이 설정되어 있지 않으므로 기본 데이터가 비공개인 경우에도 모든 애플리케이션은 제공자에서 읽고 제공자에 쓸 수 있습니다. 이를 변경하려면 매니페스트 파일에서 <provider> 요소의 속성이나 하위 요소를 사용하여 제공자의 권한을 설정합니다. 전체 제공자, 특정 테이블, 특정 레코드 또는 세 가지 모두에 적용되는 권한을 설정할 수 있습니다.

제공자의 권한은 매니페스트 파일에서 하나 이상의 <permission> 요소를 사용하여 정의합니다. 권한을 제공자에 고유하게 만들려면 android:name 속성에 자바 스타일 범위를 사용합니다. 예를 들어 읽기 권한의 이름을 com.example.app.provider.permission.READ_PROVIDER로 지정합니다.

다음 목록에서는 제공자 권한의 범위를 설명합니다. 제공자 전체에 적용되는 권한부터 시작하여 점차 권한이 세분화됩니다. 보다 세분화된 권한이 범위가 큰 권한보다 우선합니다.

단일 읽기-쓰기 제공자 수준 권한
전체 제공자의 읽기 및 쓰기 액세스 권한을 둘 다 제어하는 하나의 권한으로, <provider> 요소의 android:permission 속성으로 지정됩니다.
별도의 읽기 및 쓰기 제공자 수준 권한
전체 제공자에 대한 읽기 권한과 쓰기 권한입니다. <provider> 요소의 android:readPermission android:writePermission 속성을 사용하여 지정합니다. 이 권한은 android:permission에 필요한 권한보다 우선합니다.
경로 수준 권한
제공자의 콘텐츠 URI에 관한 읽기, 쓰기 또는 읽기/쓰기 권한입니다. <provider> 요소의 <path-permission> 하위 요소를 사용하여 제어하려는 각 URI를 지정합니다. 지정하는 콘텐츠 URI마다 읽기/쓰기 권한, 읽기 권한, 쓰기 권한 또는 세 가지 권한을 모두 지정할 수 있습니다. 읽기 및 쓰기 권한이 읽기/쓰기 권한보다 우선합니다. 또한 경로 수준 권한이 제공자 수준 권한보다 우선합니다.
임시 권한
애플리케이션에 일반적으로 필요한 권한이 없더라도 애플리케이션에 대한 임시 액세스 권한을 부여하는 권한 수준입니다. 임시 액세스 기능을 사용하면 애플리케이션이 매니페스트에서 요청해야 하는 권한 수를 줄일 수 있습니다. 임시 권한을 사용 설정하면 제공업체의 영구 권한이 필요한 애플리케이션은 모든 데이터에 지속적으로 액세스하는 애플리케이션뿐입니다.

예를 들어 이메일 제공업체와 앱을 구현하고 외부 이미지 뷰어 애플리케이션이 제공업체의 사진 첨부파일을 표시할 수 있도록 하려는 경우 필요한 권한을 고려하세요. 권한을 요청하지 않고 이미지 뷰어에 필요한 액세스 권한을 부여하려면 사진의 콘텐츠 URI 임시 권한을 설정하면 됩니다.

사용자가 사진을 표시하려고 할 때 앱이 사진의 콘텐츠 URI와 권한 플래그를 포함하는 인텐트를 이미지 뷰어에 전송하도록 이메일 앱을 설계합니다. 그러면 이미지 뷰어가 이메일 제공업체에 쿼리하여 사진을 검색할 수 있습니다. 이 뷰어에 제공자에 관한 일반적인 읽기 권한이 없더라도 무방합니다.

임시 권한을 사용 설정하려면 <provider> 요소의 android:grantUriPermissions 속성을 설정하거나 하나 이상의 <grant-uri-permission> 하위 요소를 <provider> 요소에 추가합니다. 제공자에서 임시 권한과 연결된 콘텐츠 URI 지원을 삭제할 때마다 Context.revokeUriPermission()를 호출하세요.

이 특성의 값은 제공자에 액세스할 수 있는 정도를 결정합니다. 이 속성을 "true"로 설정하면 시스템은 제공자 전체에 임시 권한을 부여하여 제공자 수준 또는 경로 수준 권한에서 요구하는 다른 권한을 재정의합니다.

이 플래그가 "false"로 설정되면 <grant-uri-permission> 하위 요소를 <provider> 요소에 추가합니다. 각 하위 요소는 임시 액세스 권한이 부여된 콘텐츠 URI(하나 또는 여러 개)를 지정합니다.

애플리케이션에 임시 액세스 권한을 위임하려면 인텐트에 FLAG_GRANT_READ_URI_PERMISSION 플래그나 FLAG_GRANT_WRITE_URI_PERMISSION 플래그 또는 둘 다를 포함해야 합니다. 이는 setFlags() 메서드를 사용하여 설정됩니다.

android:grantUriPermissions 속성이 없으면 "false"인 것으로 간주됩니다.

<provider> 요소

ActivityService 구성요소와 마찬가지로 ContentProvider의 서브클래스는 <provider> 요소를 사용하여 애플리케이션의 매니페스트 파일에 정의됩니다. Android 시스템이 이 요소에서 다음 정보를 가져옵니다.

권한(android:authorities)
시스템 내에서 전체 제공자를 식별하는 상징적인 이름입니다. 이 속성은 콘텐츠 URI 디자인 섹션에 자세히 설명되어 있습니다.
제공자 클래스 이름(android:name)
ContentProvider를 구현하는 클래스입니다. 이 클래스는 ContentProvider 클래스 구현 섹션에 자세히 설명되어 있습니다.
권한
제공자의 데이터에 액세스하기 위해 다른 애플리케이션이 보유해야 하는 권한을 지정하는 속성입니다.

권한과 권한에 상응하는 속성은 콘텐츠 제공자 권한 구현 섹션에 자세히 설명되어 있습니다.

시작 및 제어 특성
이러한 속성은 Android 시스템에서 제공자를 시작하는 방법과 시점, 제공자의 프로세스 특성 및 기타 런타임 설정을 결정합니다.
  • android:enabled: 시스템이 제공자를 시작할 수 있게 하는 플래그입니다.
  • android:exported: 다른 애플리케이션이 이 제공자를 사용하도록 허용하는 플래그
  • android:initOrder: 이 제공자가 시작된 순서(동일한 프로세스 내의 다른 제공자와 비교하여)
  • android:multiProcess: 시스템이 클라이언트를 호출하는 것과 동일한 프로세스에서 제공자를 시작할 수 있게 하는 플래그입니다.
  • android:process: 제공자가 실행되는 프로세스의 이름
  • android:syncable: 제공자의 데이터가 서버의 데이터와 동기화되어야 함을 나타내는 플래그

이러한 속성은 <provider> 요소 가이드에 완전히 설명되어 있습니다.

정보성 특성
제공업체의 아이콘 및 라벨(선택사항)
  • android:icon: 제공자의 아이콘이 포함된 드로어블 리소스입니다. 이 아이콘은 Settings > Apps > All의 앱 목록에서 제공자의 라벨 옆에 표시됩니다.
  • android:label: 제공자, 제공자 데이터 또는 둘 다를 설명하는 정보 라벨입니다. 라벨이 설정 > > 전체의 앱 목록에 표시됩니다.

이러한 속성은 <provider> 요소 가이드에 완전히 설명되어 있습니다.

인텐트 및 데이터 액세스

애플리케이션은 Intent를 사용하여 간접적으로 콘텐츠 제공자에 액세스할 수 있습니다. 애플리케이션이 ContentResolver 또는 ContentProvider의 메서드를 호출하지 않습니다. 대신, 활동을 시작하는 인텐트를 전송합니다. 이 인텐트는 대개 제공자가 소유한 애플리케이션의 일부입니다. 대상 활동이 UI에서 데이터를 검색하고 표시하는 역할을 합니다.

인텐트의 작업에 따라 대상 활동은 사용자에게 제공자의 데이터를 수정하라는 메시지를 표시할 수도 있습니다. 인텐트에는 대상 활동이 UI에 표시하는 '추가' 데이터가 포함될 수도 있습니다. 그러면 사용자는 데이터를 변경할 수 있는 옵션을 선택한 다음 데이터를 사용하여 제공자의 데이터를 수정할 수 있습니다.

인텐트 액세스를 사용하여 데이터 무결성을 지원할 수 있습니다. 엄격하게 정의된 비즈니스 로직에 따라 데이터를 삽입, 업데이트 및 삭제하는 것이 제공업체가 의존할 수 있습니다. 이 경우 다른 애플리케이션이 데이터를 직접 수정하도록 허용하면 데이터가 잘못될 수 있습니다.

개발자들에게 인텐트 액세스 사용을 허용하려면, 그 내용을 철저히 기록해두어야 합니다. 애플리케이션의 UI를 사용한 인텐트 액세스가 코드로 데이터를 수정하려고 시도하는 것보다 나은 이유를 설명합니다.

제공자의 데이터를 수정하려는 수신 인텐트 처리도 다른 인텐트 처리와 다르지 않습니다. 인텐트 사용에 관한 자세한 내용은 인텐트 및 인텐트 필터를 참고하세요.

관련 추가 정보는 캘린더 제공자 개요를 참고하세요.