컨텐츠 제공자 기본사항

kosdjs·2025년 7월 2일
0

Android

목록 보기
15/24
  • 컨텐츠 제공자는 데이터의 중앙 저장소로의 접근을 관리함, 안드로이드 앱의 일부로 데이터와 작업하기 위한 고유 UI를 제공하는 제공자가 있지만, 컨텐츠 제공자는 제공자 클라이언트 객체를 이용해 제공자에 접근하는 것처럼 주로 다른 앱에 의해 사용됨, 제공자와 제공자 클라이언트는 IPC와 데이터 접근 보안을 위해 데이터의 일관된 표준 인터페이스를 제공함

  • 다른 앱에 존재하는 컨텐츠 제공자에 접근하기 위한 코드를 구현하거나, 다른 앱과 데이터를 공유하기 위해 새로운 컨텐츠 제공자를 생성하는 작업 중 하나를 일반적으로 하게 되고 이 페이지는 이미 존재하는 컨텐츠 제공자와 작업하기 위한 기초사항에 대한 문서임

개요

  • 컨텐츠 제공자는 데이터를 외부 앱에 하나 이상의 테이블로 제공하며 이는 관계형 데이터베이스의 테이블과 비슷함, 행은 제공자가 수집하는 특정 데이터 유형의 인스턴스를 나타내고, 행의 각 열은 인스턴스를 위해 수집된 개별 데이터 조각을 나타냄

  • 컨텐츠 제공자는 다음 내용을 포함하는 아래 그림과 같이 여러가지 API와 컴포넌트를 위해 앱의 데이터 저장소 계층에 대한 접근을 조정함

    • 다른 앱과 앱의 데이터에 접근을 공유
    • 위젯에 데이터 전송
    • SearchRecentSuggestionsProvider를 사용한 검색 프레임워크를 통해 앱에 사용자 지정 추천 검색어을 반환
    • AbstractThreadedSyncAdapter의 구현체를 사용해 앱 데이터를 서버와 동기화
    • CursorLoader를 사용해 UI에 데이터 로드

제공자에 접근

  • 앱의 컨텍스트에 있는 ContentResolver 객체를 사용해 클라이언트로서 제공자와 통신해 컨텐츠 제공자의 데이터에 접근할 수 있음, ContentResolver 객체는 ContentProvider 를 구현한 클래스의 인스턴스인 제공자 객체와 통신할 수 있음

  • 제공자 객체는 클라이언트로부터 데이터 요청을 받아 수행한 후 결과를 반환함, ContentResolver의 객체는 제공자 객체(ContentProvider의 구체적인 자식클래스의 인스턴스)의 메소드를 호출하는 동일한 이름의 메소드를 가지고 있음, ContentResolver의 메소드는 저장소의 기본적인 CRUD (create, retrieve, update, delete) 기능을 제공함

  • UI에서 컨텐츠 제공자에 접근하려면 보통 CursorLoader 를 사용해 백그라운드에서 비동기 쿼리를 실행함, 이렇게 하면 쿼리를 실행하는 동안에도 사용자가 UI를 사용 가능함, 이 경우에 다음 그림과 같이 액티비티나 프래그먼트의 UI에서 CursorLoader를 쿼리를 실행하기 위해 호출하면 ContentResolver를 사용해 ContentProvider를 가져옴

노트: 제공자에 접근하려면 앱은 보통 제공자의 매니페스트 파일에 있는 특정 권한을 요청해야 합니다. 이 개발 패턴은 뒤에 나올 컨텐츠 제공자 권한 부분에서 자세히 설명합니다.

  • 사용자가 보관하고 싶어하는 비표준 단어를 저장하는 사용자 사전 제공자는 안드로이드 플랫폼에 내장된 제공자 중 하나임

사용자 사전 제공자 테이블 예시

wordapp idfrequencylocale_ID
mapreduceuser1100en_US1
precompileruser14200fr_FR2
appletuser2225fr_CA3
constuser1255pt_BR4
intuser5100en_UK5
  • 위의 표에서 한 행은 표준 사전에서 찾지 못한 단어의 인스턴스를 나타냄, 각 열은 처음 단어를 마주친 언어와 같이 해당 단어의 데이터 조각을 나타냄, 열의 헤더는 제공자에 저장되는 열의 이름임, 예를 들어 행의 언어를 확인하고 싶다면, 행의 locale 열을 확인하면 됨, 이 제공자의 경우 _ID 열이 제공자가 자동적으로 유지하는 기본 키임

  • ContentResolver.query()를 호출해 사용자 사전 제공자에서 단어와 그 단어의 언어 리스트를 얻을 수 있음, 이 메소드는 사용자 지정 제공자에 정의된 ContentProvider.query() 메소드를 호출함

ContentResolver.query()를 사용하는 예시

// UserDictionary 쿼리하고 결과를 반환함
cursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI,  // 단어 테이블의 컨텐츠 URI
        projection,                        // 각 행에 대해 반환할 열
        selectionClause,                   // 선택 기준
        selectionArgs.toTypedArray(),      // 선택 기준
        sortOrder                          // 반환된 행의 정렬 순서
)

query()를 SQL의 SELECT 구문과 비교하는 테이블

query() 인수SELECT 키워드/매개 변수참고
UriFROM table_nameUri는 제공자에서 table_name으로 명명된 테이블에 매핑됨
projectioncol,col,col,...projection은 검색된 각 행에 포함될 열의 배열임
selectionWHERE col = valueselection은 행 선택 기준을 지정함
selectionArgs정확한 동일어 없음selectionArgs는 SELECT 구문에 있는 ? 자리표시자를 대체합니다.
sortOrderORDER BY col,col,...sortOrder는 반환된 Cursor에서 행이 나타나는 순서를 지정함

컨텐츠 URI

  • 컨텐츠 URI는 제공자에서 데이터를 식별하는 URI임, 전체 제공자의 상징적 이름('authority')과 테이블을 가리키는 이름(경로)를 포함함, 클라이언트에서 제공자의 테이블에 접근하는 메소드를 호출할 때 테이블의 컨텐츠 URI는 인수 중 하나임

  • 앞의 코드에서 CONTENT_URI 상수는 사용자 사전 제공자의 Words 테이블의 컨텐츠 URI를 포함하고 있음, ContentResolver 객체는 URI의 authority를 파싱하고 이를 시스템의 알려진 제공자 테이블과 비교해 제공자를 확인함, 그러면 ContentResolver 는 정확한 제공자에게 쿼리를 전달할 수 있음

  • ContentProvider 는 컨텐츠 URI의 경로 부분을 접근할 테이블을 선택하는데 사용함, 일반적으로 제공자는 외부에 접근을 허용하는 각 테이블마다 경로를 가지고 있음

이전 코드에서 Words 테이블의 URI

content://user_dictionary/words
  • content:// 문자열은 스키마이며, 항상 표시되며 이를 콘텐츠 URI로 식별함

  • user_dictionary 문자열은 제공자의 authority

  • words 문자열은 테이블의 경로임

  • 많은 제공자들은 URI의 끝에 ID 값을 추가해 테이블의 단일 행에 접근할 수 있게 함

사용자 사전 제공자에서 _ID가 4인 행을 검색하고 싶을 때 사용하는 컨텐츠 URI 예시

val singleUri: Uri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4)
  • 일련의 행을 검색한 다음 그중 하나를 수정하거나 삭제하고자 하는 경우 종종 ID 값을 사용함

노트: UriUri.Builder 클래스들은 올바른 URI 객체를 문자열로 구성하기 위한 편리한 메소드들을 포함하고 있습니다. ContentUris 클래스는 URI에 ID 값을 추가하기 위한 편리한 메소드들을 포함하고 있습니다. 이전의 코드는 withAppendedID() 를 사용자 사전 제공자의 컨텐츠 URI에 ID를 추가하기 위해 사용했습니다.

제공자에서 데이터 검색

명확성을 위해서 이 부분의 코드에서 ContentResolver.query()를 UI 스레드에서 호출합니다. 하지만 실제 코드에서는 쿼리를 별도의 스레드에서 비동기적으로 처리하세요. CursorLoader 클래스를 사용할 수도 있습니다. 또한 코드는 일부분이며 전체 앱을 보여주지 않습니다.

제공자에서 데이터를 검색하는 단계

  1. 제공자에 대한 읽기 권한 요청

  2. 제공자에게 쿼리를 보내는 코드 정의

읽기 권한 요청

  • 제공자에서 데이터를 검색하려면 앱은 제공자에 대해 읽기 권한이 필요함, 이 권한은 런타임에 요청할 수 없고 대신 매니페스트에 <uses-permission> 속성과 제공자에 선언된 정확한 권한 이름을 사용해 필요한 권한을 지정할 수 있음

  • 매니페스트에 <uses-permission> 속성을 지정하면 앱이 권한을 요청한다는 것을 뜻하고, 사용자가 앱을 설치하면 암시적으로 이 요청을 수락하는 것임

  • 제공자에서 사용하는 권한의 정확한 이름을 알아보려면 권한의 문서를 확인해야 함

  • 사용자 사전 제공자는 android.permission.READ_USER_DICTIONARY 권한을 매니페스트에 정의하므로, 이 제공자를 읽기 원하는 앱은 이 권한을 반드시 요청해야 함

쿼리 구성

사용자 사전 제공자에 접근하기 위한 몇 가지 변수 예시

// "projection"은 각 행에 대해 반환될 열을 정의합니다.
private val mProjection: Array<String> = arrayOf(
        UserDictionary.Words._ID,    // _ID 열 이름에 대한 Contract 클래스 상수
        UserDictionary.Words.WORD,   // word 열 이름에 대한 Contract 클래스 상수
        UserDictionary.Words.LOCALE  // locale 열 이름에 대한 Contract 클래스 상수
)

// 선택 절에 포함할 문자열을 정의합니다.
private var selectionClause: String? = null

// 선택 인수를 포함할 배열을 선언합니다.
private lateinit var selectionArgs: Array<String>
  • 제공자 클라이언트 쿼리는 SQL 쿼리와 비슷하고 반환해야할 열의 집합, 선택 조건의 집합, 정렬 순서를 포함함

  • 쿼리가 반환해야할 열의 집합을 projection 이라고 하고 예시 코드에서는 mProjection 변수로 나타냄

  • 검색할 행을 지정하는 표현식은 선택 절과 선택 인수로 나뉨, 선택 절은 논리 표현식과 열의 이름, 값의 조합이고 예시 코드에서 mSelectionClause 변수로 나타냈음, 만약 값 대신 대체 가능한 매개변수 ? 를 지정한다면 쿼리 메소드는 선택 인수 배열의 값을 사용해 검색함, 이 때 선택 인수 배열은 예시 코드에서 mSelectionArgs 변수로 나타냈음

사용자가 검색할 단어을 입력함에 따라 선택 절을 다르게 사용하는 예시

/*
 * 선택 인수를 포함할 배열을 선언합니다.
 */
private lateinit var selectionArgs: Array<String>

// UI에서 단어를 가져옵니다.
searchString = searchWord.text.toString()

// 유효하지 않거나 악의적인 입력을 확인하는 코드를 여기 입력하세요.

// 입력 받은 문자열이 비어있다면 모든 행을 가져옵니다.
selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let {
    selectionClause = "${UserDictionary.Words.WORD} = ?"
    arrayOf(it)
} ?: run {
    selectionClause = null
    emptyArray<String>()
}

// 테이블에 대해 쿼리를 수행하고 Cursor 객체를 반환합니다.
mCursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI, // words 테이블의 컨텐츠 URI
        projection,                       // 각 행에 대해 반환할 열
        selectionClause,                  // null 또는 사용자가 입력한 단어
        selectionArgs,                    // 비어 있거나 사용자가 입력한 문자열
        sortOrder                         // 반환된 행의 정렬 순서
)

// 일부 제공자는 오류 발생시 null을 반환하고, 나머지 제공자는 예외를 발생시킴
when (mCursor?.count) {
    null -> {
        /*
         * 여기에 오류를 처리하는 코드를 입력하세요. cursor를 사용하지 않도록 주의하세요!
         * android.util.Log.e()를 사용해 이 오류를 로그로 기록할 수 있습니다.
         */
    }
    0 -> {
        /*
         * 여기에 사용자에게 검색이 실패했다는 것을 알릴 코드를 입력하세요.
         * 이는 일반적으로 오류가 아닙니다. 사용자에게 새 행 삽입이나
         * 검색어 재입력 옵션을 제공할 수 있습니다.
         */
    }
    else -> {
        // 여기에 결과를 처리하는 코드를 입력하세요.
    }
}

위 코드와 유사한 SQL 구문

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

악의적인 입력 방지

  • 만약 컨텐츠 제공자의 데이터가 SQL 데이터베이스로 관리되고 있다면, 외부의 신뢰할수 없는 데이터를 SQL 구문에 포함시키면 SQL 주입 공격으로 이어질 수 있음

위 상황에 대한 예시

// 사용자의 입력을 열의 이름으로 직접 연결하는 selection 절을 구성
var selectionClause = "var = $mUserInput"
  • 이렇게 하면 사용자가 SQL 구문에 악의적인 SQL을 잠재적으로 연결할 수 있음, 예시로 사용자가 nothing; DROP TABLE *; 이라고 mUserInput에 입력한다면 결과적으로 var = nothing; DROP TABLE *; 이 선택 절이 될 수 있음, 이 결과로 제공자가 SQL 주입 공격에 대한 대비가 되어 있지 않다면 데이터베이스의 모든 테이블을 삭제를 하는 결과로 이어질 수 있음

  • 이런 문제를 피하려면 대체 가능한 매개 변수인 ? 와 별도의 선택 인수 배열을 선택 절에 사용해야 함, 이 방식을 사용하면 사용자 입력이 SQL 구문으로 처리되지 않고 쿼리에 연결됨, SQL으로 처리되지 않기 때문에 사용자는 악의적인 SQL을 주입할 수 없음

SQL 주입 공격을 피하기 위한 선택 절과 인수의 예시

// 대체 가능한 매개 변수을 사용해 선택 절을 구성합니다.
var selectionClause = "var = ?"

// 선택 인수를 포함할 가변 리스트를 정의합니다.
var selectionArgs: MutableList<String> = mutableListOf()

// 선택 인수에 사용자 입력을 추가합니다.
selectionArgs += userInput
  • 이 방법은 제공자가 SQL 데이터베이스를 사용하지 않을 때에도 사용하는 방법임

쿼리 결과 표시

  • ContentResolver.query() 클라이언트 메소드는 항상 쿼리의 선택 조건에 맞는 행과 쿼리의 projection에서 지정한 열만 포함한 Cursor를 반환함, Cursor 객체는 본인이 포함하는 행과 열의 랜덤 읽기를 제공함

  • Cursor 메소드를 사용하면 결과로 나온 행을 반복(iterate)하거나, 각 열의 데이터 타입을 확인하거나, 열에서 데이터를 가져오거나, 결과의 다른 속성들(행/열 개수, 커서 위치 등)을 확인할 수 있음

  • 일부 Cursor 구현체는 제공자의 데이터가 변경되었을 때 자동적으로 객체를 수정하거나, Cursor 가 변경되었을 때 옵저버 객체의 메소드를 트리거하거나, 둘 다 수행함

노트: 제공자는 쿼리를 수행하는 객체의 특성에 따라 열에 대한 접근을 제한할 수 있습니다. 예를 들어 연락처 제공자(Contacts Provider)는 일부 열을 동기화 어댑터에서만 접근할 수 있게 제한하기 때문에 액티비티나 서비스에는 해당 열을 반환하지 않습니다.

  • 만약 선택 조건에 맞는 행이 없다면 제공자는 Cursor.getCount() 의 값이 0인 즉 빈 Cursor 객체를 반환함

  • 만약 내부 오류가 발생하면 제공자에 따라 쿼리의 결과가 다르게 나옴, null을 반환하거나 예외를 발생시킴

  • Cursor 가 행의 리스트이기 때문에 SimpleCursorAdapter 를 사용해 ListView 에 연결하는 방법은 내용을 표시하기 좋은 방법임

앞선 예시들을 통해 쿼리에서 전달된 Cursor 를 포함한 SimpleCursorAdapter 를 생성해 ListView의 어댑터로 설정하는 예시

// Cursor에서 가져와 출력 행에 로드할 열 목록을 정의합니다
val wordListColumns : Array<String> = arrayOf(
        UserDictionary.Words.WORD,      // word 열 이름에 해당하는 Contract 클래스 상수
        UserDictionary.Words.LOCALE     // locale 열 이름에 해당하는 Contract 클래스 상수
)

// 각 행에서 Cursor 열을 받을 View ID의 리스트를 정의합니다.
val wordListItems = intArrayOf(R.id.dictWord, R.id.locale)

// Creates a new SimpleCursorAdapter
cursorAdapter = SimpleCursorAdapter(
        applicationContext,             // 애플리케이션의 Context 객체
        R.layout.wordlistrow,           // ListView의 한 행에 대한 XML 레이아웃
        mCursor,                        // 쿼리 결과
        wordListColumns,                // 커서의 열 이름 배열
        wordListItems,                  // 행 레이아웃의 View ID 배열
        0                               // 플래그 (일반적으로 필요 없음)
)

// ListView의 어댑터를 설정합니다.
wordList.setAdapter(cursorAdapter)

노트: CursorListView 에서 사용하려면 이름이 _ID 인 열을 반드시 포함해야 합니다. 이는 이전 코드에서 리스트 뷰에서 표시하지 않더라도 쿼리에서 Words 테이블의 _ID 열을 받는 이유입니다. 또한 제공자가 각 테이블마다 _ID 열을 가지고 있는 이유이기도 합니다.

쿼리 결과에서 데이터 가져오기

  • 쿼리 결과를 표시하는 것 외에 다른 작업에 사용할 수도 있음, 예를 들면 사용자 사전 제공자에서 스펠링을 전달받아 다른 제공자에서 찾아볼 수 있음

Cursor 에서 데이터를 가져오는 예시

/*
* Cursor가 유효할 때만 실행합니다. 사용자 사전 제공자는 내부 오류가 발생하면
* null을 반환합니다. 다른 제공자는 null을 반환하는 대신 예외가 발생할 수 있습니다.
*/
mCursor?.apply {
    // "word" 열의 인덱스를 결정합니다.
    val index: Int = getColumnIndex(UserDictionary.Words.WORD)

    /*
     * cursor의 다음 행으로 이동합니다. 행을 이동하기 전의 포인터는 -1 이므로,
     * 이 때 데이터를 가져오려고 시도하면 예외가 발생합니다.
     */
    while (moveToNext()) {
        // 열의 값을 가져옵니다.
        newWord = getString(index)

        // 여기에 가져온 단어를 처리하는 코드를 입력하세요.
        ...
        // 반복문의 끝
    }
}
  • Cursor 구현체는 객체에서 다양한 타입의 데이터를 받기 위한 여러개의 get 메소드를 가지고 있음, 예시에서는 getString() 을 사용했지만, getType() 메소드를 사용해 열의 데이터 타입이 나타내는 값을 가져올 수 있음

쿼리 결과 리소스 해제

  • Cursor 객체는 연계된 리소스가 더 빨리 해제되게 하기 위해서 사용이 끝나면 반드시 해제해야 함, close()를 호출하거나, 자바의 try-with-resources 구문을 사용하거나, 코틀린의 use() 함수를 사용해 Cursor 를 사용 후 해제할 수 있음

컨텐츠 제공자 권한

  • 제공자가 있는 앱은 제공자의 다른 앱이 데이터에 접근하기 위해 반드시 가지고 있어야 하는 권한을 지정할 수 있음, 이런 권한들은 앱이 어떤 데이터에 접근하려고 하는지 사용자가 알 수 있게 함, 다른 앱은 제공자에 접근하기 위한 권한을 요청하고, 사용자는 앱을 설치할 때 요청되는 권한을 볼 수 있음

  • 제공자가 있는 앱에서 아무 권한도 지정하지 않는다면 제공자가 외부에 공개(exported)되지 않는 한, 다른 앱은 제공자의 데이터에 접근할 수 없음, 제공자가 있는 앱의 다른 컴포넌트는 권한이 없어도 항상 읽기와 쓰기가 가능함

  • 사용자 사전 제공자는 데이터를 읽기 위해서 android.permission.READ_USER_DICTIONARY권한이 필요하고 데이터 삽입, 수정, 삭제를 위해서는 android.permission.WRITE_USER_DICTIONARY 권한이 필요함

  • 제공자에 접근하기 위해 권한을 요청하려면 매니페스트 파일에서 <uses-permission> 속성을 사용하면 됨

    • 안드로이드 패키지 매니저가 앱을 설치할 때 사용자는 앱이 요청하는 권한을 반드시 승인해야 함, 사용자가 권한을 승인하면 패키지 매니저는 설치를 계속하고, 승인하지 않는다면 설치를 멈춤 -> 현재는 위험한 권한을 실행시 요청받는 방식으로 변경됨으로 적용되지 않는 방법임

<uses-permission> 속성을 사용해 사용자 사전 제공자의 읽기 접근을 요청하는 예시

<uses-permission android:name="android.permission.READ_USER_DICTIONARY">

데이터 삽입, 수정, 삭제

  • 제공자에서 데이터를 읽는 방식과 동일하게 데이터를 수정할 수 있음, ContentResolver 의 메소드를 인수와 함께 호출하면 ContentProvider 의 대응되는 메소드에 전달됨, 이 방식으로 제공자와 클라이언트가 자동적으로 IPC 와 보안을 처리함

데이터 삽입

  • ContentResolver.insert() 메소드를 사용해 제공자에 데이터를 삽입할 수 있음, 이 메소드는 제공자에 새 행을 삽입하고 그 행에 대한 컨텐츠 URI를 반환함

사용자 사전 제공자에 새 단어를 삽입하는 예시

// 삽입의 결과로 받는 새 Uri 객체를 정의합니다.
lateinit var newUri: Uri
...
// 삽입하기 위한 새 값을 포함하는 객체를 정의합니다.
val newValues = ContentValues().apply {
    /*
     * 각 열에 들어갈 값을 설정하고 삽입합니다.
     * put 메소드의 인수는 열의 이름과 그 열에 들어갈 값입니다.
     */
    put(UserDictionary.Words.APP_ID, "example.user")
    put(UserDictionary.Words.LOCALE, "en_US")
    put(UserDictionary.Words.WORD, "insert")
    put(UserDictionary.Words.FREQUENCY, "100")

}

newUri = contentResolver.insert(
        UserDictionary.Words.CONTENT_URI,   // 사용자 사전의 컨텐츠 URI
        newValues                           // 삽입할 값
)
  • 새 행의 데이터는 단일 ContentValues 객체에 들어가며 이는 단일 행의 cursor 와 비슷한 형태임, 이 객체의 열에는 모든 값의 데이터 유형이 같을 필요가 없으며, 값을 지정하고 싶지 않다면 ContentValues.putNull() 을 사용해 null 로 설정할 수 있음

  • 예시에서 _ID 열을 추가하지 않은 이유는 이 열은 자동적으로 유지되기 때문임, 제공자는 새롭게 추가되는 행의 _ID 에 고유한 값을 할당함, 제공자는 이 값을 테이블의 기본 키로 사용함

데이터 삽입 후 반환되는 URI의 예시

content://user_dictionary/words/<id_value>
  • <id_value> 는 새 행의 _ID 의 값임, 대부분의 제공자가 이런 형식의 컨텐츠 URI를 자동적으로 감지해 해당 행을 찾아 작업을 수행함

  • ContentUris.parseId() 를 사용해 반환된 Uri 로부터 _ID 의 값을 가져올 수 있음

데이터 수정

  • 데이터를 수정하려면 삽입에서 했던 것처럼 수정할 값을 포함하는 ContentValues 객체를 사용하고 쿼리를 사용했던 것처럼 선택 조건을 사용함, ContentResolver.update() 메소드를 사용하고 ContentValues 객체에는 수정할 열의 값만 있으면 됨, 만약 열의 내용을 지우고 싶다면 값을 null 로 설정하면 됨

언어에 en 을 가지고 있는 모든 행을 null 로 수정하고 수정된 행의 수를 반환받는 예시

// 수정할 값을 포함하는 객체를 정의합니다.
val updateValues = ContentValues().apply {
    /*
     * 수정할 값을 설정하고 선택된 단어를 수정합니다.
     */
    putNull(UserDictionary.Words.LOCALE)
}

// 수정하고 싶은 행의 선택 조건을 정의합니다.
val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?"
val selectionArgs: Array<String> = arrayOf("en_%")

// 수정된 행의 수를 포함할 변수를 정의합니다.
var rowsUpdated: Int = 0
...
rowsUpdated = contentResolver.update(
        UserDictionary.Words.CONTENT_URI,  // 사용자 사전의 컨텐츠 URI
        updateValues,                      // 수정할 열
        selectionClause,                   // 선택할 열
        selectionArgs                      // 비교할 값
)
  • 악의적인 입력 방지 부분에서 다루었듯 ContentResolver.update() 를 호출할 때 ? 를 사용함

데이터 삭제

  • 행을 삭제하는 것은 행을 검색하는 것과 비슷함, 삭제하고 싶은 행의 선택 조건을 지정하면 메소드는 삭제된 행의 수를 반환함

APP_IDuser와 일치하는 행을 삭제하는 예시

// 삭제하고 싶은 행의 선택 조건을 정의합니다.
val selectionClause = "${UserDictionary.Words.APP_ID} LIKE ?"
val selectionArgs: Array<String> = arrayOf("user")

// 삭제된 행의 수를 포함할 변수를 정의합니다.
var rowsDeleted: Int = 0
...
// 선택 조건과 일치하는 단어를 삭제함
rowsDeleted = contentResolver.delete(
        UserDictionary.Words.CONTENT_URI,  // 사용자 사전의 컨텐츠 URI
        selectionClause,                   // 선택할 열
        selectionArgs                      // 비교할 값
)
  • 악의적인 입력 방지 부분에서 다루었듯 ContentResolver.delete() 를 호출할 때 ? 를 사용함

제공자 데이터 유형

  • 컨텐츠 제공자는 매우 다양한 데이터 유형을 제공함

    • 정수 (integer)

    • 큰 정수 (long)

    • 소수 (float)

    • 세밀한 소수 (double)

  • 제공자에서 자주 사용되는 다른 데이터 유형은 64KB 배열로 구현되는 BLOB (binary large object)가 있음, 사용 가능한 데이터 유형은 Cursor 클래스의 get 메소드를 사용해 확인할 수 있음

  • 제공자에서 각 열의 데이터 유형은 보통 문서에 정리되어 있음, 사용자 사전 제공자의 데이터 유형은 계약 클래스인 UserDictionary.Words 클래스의 문서에 정리되어 있음, Cursor.getType() 을 호출해 데이터 유형을 확인할 수도 있음

  • 제공자는 정의하는 각 컨텐츠 URI에 대해 MIME 데이터 유형 정보도 유지함, MIME 데이터 유형 정보를 사용해 앱이 제공자가 제공하는 데이터를 처리할 수 있는지 확인하거나 MIME 유형에 기반해 처리할 수 있음

  • 보통 MIME 유형은 복잡한 데이터 구조나 파일을 포함하는 제공자를 사용할 때 필요함, 예시로 연락처 제공자의 ContactsContract.Data 테이블은 각 행에 저장된 연락처 데이터의 유형을 표시하기 위해 MIME 유형을 사용함, ContentResolver.getType() 을 호출해 해당하는 컨텐츠 URI의 MIME 유형을 받을 수 있음

제공자 접근의 대체 방식

  • 앱 개발에 있어서 제공자에 접근할 수 있는 다음 세 가지 대체 방식은 중요함
    • 일괄 접근: ContentProviderOperation 클래스의 메소드를 사용해 일괄 접근 호출을 생성하고 ContentResolver.applyBatch() 에 적용할 수 있음
    • 비동기 쿼리: 쿼리를 별도의 스레드에서 사용함, CursorLoader 객체를 사용할 수 있음
    • 인텐트를 사용한 데이터 접근: 제공자에게 직접 인텐트를 전달할 수 없지만 제공자를 가지고 있는 앱에 인텐트를 전달할 수는 있음, 이는 보통 제공자의 데이터를 수정하는 가장 적합한 방법임

일괄 접근

  • 일괄 접근은 한 번에 많은 수의 행을 삽입하거나, 한 메소드에서 여러 테이블에 행을 삽입할 때, 그리고 일반적으로 프로세스 경계를 넘어 여러 작업을 하나의 트랜잭션(즉, 원자적 연산)으로 처리할 때 유용함

  • 일괄 모드로 제공자에 접근하려면 ContentProviderOperation 객체의 배열을 생성하고 ContentResolver.applyBatch() 를 사용해 컨텐츠 제공자에 전달해야 함, 이 메소드에서는 특정 컨텐츠 URI를 보내기 보다는 컨텐츠 제공자의 authority 를 전달하는 게 좋음, 이를 통해 배열의 각 ContentProviderOperation 객체가 다른 테이블에 대해 작업할 수 있음, ContentResolver.applyBatch() 는 결과의 배열을 반환함

  • ContactContract.RawContacts의 설명에서 일괄 접근을 하는 예시 코드가 있음

인텐트를 사용한 데이터 접근

  • 인텐트는 컨텐츠 제공자로 접근하는 간접적인 방법을 제공함, 앱이 접근 권한을 가지고 있지 않아도 권한을 가지고 있는 다른 앱에서 결과를 인텐트로 받거나 권한을 가지고 있는 앱을 활성화해 사용자가 그 앱에서 작업하게 하는 것으로 사용자가 제공자의 데이터에 접근하게 할 수 있음

임시 권한으로 접근하기

  • 적합한 접근 권한이 없더라도 권한이 있는 앱에 인텐트를 보내고 URI 권한이 포함된 결과 인텐트를 받아서 컨텐츠 제공자의 데이터에 접근할 수 있음, 이 권한은 특정 컨텐츠 URI로 지정되며 전달받는 액티비티가 끝나기 전까지만 유효함, 영구 권한이 있는 앱은 결과 인텐트에 다음과 같이 플래그를 설정해 임시 권한을 허가함

    • 읽기 권한: FLAG_GRANT_READ_URI_PERMISSION

    • 쓰기 권한: FLAG_GRANT_WRITE_URI_PERMISSION

노트: 이 플래그들은 컨텐츠 URI에 포함된 authority가 나타내는 제공자의 일반적인 읽기 또는 쓰기 권한을 주지 않습니다. 오직 URI 자체에만 접근할 수 있습니다.

  • 컨텐츠 URI를 다른 앱에 전송할 때 이 플래그들 중 하나를 포함해야 함, 안드로이드 11(API 레벨 30)이상을 타겟팅하는 앱이 해당 플래그가 있는 인텐트를 전달 받으면 플래그에 따라 컨텐츠 URI가 나타내는 데이터를 읽거나 쓸 수 있고, URI의 authority 와 일치하는 컨텐츠 제공자를 포함하는 앱의 패키지 가시성을 얻음

  • 제공자는 매니페스트에서 <provider> 요소의 android:grantUriPermissions 속성 또는 <provider> 요소의 하위 요소인 <grant-uri-permission> 를 사용해 컨텐츠 URI의 URI 권한을 정의함

  • 예시로, READ_CONTACTS 권한이 없더라도 연락처 제공자의 연락처 정보를 검색할 수 있음, 앱에서 생일 축하 메시지를 연락처로 보내고 싶을 수 있음, 모든 사용자의 연락처의 정보에 접근할 수 있는 READ_CONTACTS 권한을 요청하는 대신, 다음과 같은 방식으로 사용자가 어떤 연락처를 앱에서 사용할 것인지 제어할 수 있음

  1. ACTION_PICK 동작과 contacts MIME 유형 CONTENT_ITEM_TYPE 을 포함하는 인텐트를 startActivityForResult() 메소드를 사용해 보냄

  2. 해당 인텐트가 연락처 앱의 선택 액티비티의 인텐트 필터에 맞기 때문에 선택 액티비티가 포그라운드로 올라오게 됨

  3. 선택 액티비티에서 사용자가 업데이트할 연락처를 선택함, 이 때 선택 액티비티는 setResult(resultcode, intent) 를 호출해 원래 앱으로 인텐트를 다시 전달함, 전달되는 인텐트에는 사용자가 선택한 연락처의 컨텐츠 URI와 FLAG_GRANT_READ_URI_PERMISSION 플래그가 포함되어 있음, 이 플래그는 앱이 컨텐츠 URI가 가리키는 연락처의 데이터를 읽을 수 있게 함, 선택 액티비티는 마지막으로 finish()를 호출해 원래 앱으로 제어를 반환함

  4. 원래 액티비티로 돌아와서 시스템이 액티비티의 onActivityResult() 메소드를 호출함, 이 메소드는 연락처의 선택 액티비티에서 생성된 결과 인텐트를 전달받음

  5. 매니페스트에서 영구적인 읽기 권한을 요청하지 않았음에도 결과 인텐트의 컨텐츠 URI를 사용해 연락처의 생일 정보나 이메일 주소를 읽어 생일 축하 메시지를 보낼 수 있게 됨

다른 앱 사용하기

  • 접근 권한이 없을 때 사용자가 데이터를 수정하게 할 다른 방법은 권한이 있는 다른 앱을 활성화 시켜 사용자가 해당 앱에서 작업하게 하는 것임

  • 예시로, 달력 앱은 ACTION_INSERT 인텐트를 받아 달력 앱의 삽입 UI를 활성화할 수 있음, 이 인텐트에 데이터를 extra로 전달해 앱의 UI를 미리 채울 수 있음, 반복 일정을 제공자에 직접 데이터를 삽입하는 구문은 복잡하기 때문에 달력 앱에 ACTION_INSERT 인텐트를 전달해 사용자가 달력 앱에서 일정을 입력하게 하는 것이 더 선호되는 방법임

도우미 앱을 사용해 데이터 표시

  • 앱이 접근 권한을 가지고 있음에도 다른 앱에서 데이터를 표시하기 위해 인텐트를 사용할 수 있음, 예를 들어 달력 앱은 ACTION_VIEW 인텐트를 받아 특정 일정을 표시함, 이는 UI를 생성하지 않고 달력 정보를 표시함

  • 인텐트를 전달하는 앱이 제공자와 관련된 앱일 필요는 없음, 예를 들어 연락처 제공자에서 연락처를 검색하고 이미지 뷰어 앱에 ACTION_VIEW 인텐트를 전달해 연락처의 이미지를 볼 수 있음

계약 클래스

  • 계약 클래스는 컨텐츠 URI, 열 이름, 인텐트 동작 그리고 컨텐츠 제공자의 다른 기능을 앱이 작업하는데 도울 수 있게 상수를 정의함, 계약 클래스는 제공자에 자동적으로 포함되지 않음, 제공자를 개발하는 개발자가 정의해야 하고 다른 개발자가 사용하게 해야 함, 안드로이드 플랫폼에서 제공되는 제공자는 android.provider 패키지에 해당하는 계약 클래스가 포함되어 있음

  • 예시로, 사용자 사전 제공자는 컨텐츠 URI와 열 이름 상수를 포함하는 UserDictionary 계약 클래스를 가지고 있음, Words 테이블의 컨텐츠 URI는 UserDictionary.Words.CONTENT_URI 상수에 정의되어 있음, UserDictionary.Words 클래스는 이전 예시 코드에서 알 수 있듯이 열의 이름 상수도 포함하고 있음

UserDictionary.Words 클래스에서 열 이름 상수를 사용해 쿼리 projection을 정의하는 예시

val projection : Array<String> = arrayOf(
        UserDictionary.Words._ID,
        UserDictionary.Words.WORD,
        UserDictionary.Words.LOCALE
)
  • 다른 계약 클래스는 연락처 제공자의 ContactsContract가 있음, 문서에 이 클래스의 예제 코드가 포함되어 있음, 자식 클래스 중 ContactsContract.Intents.Insert 계약 클래스는 인텐트와 인텐트 데이터에 대한 상수를 포함함

MIME 유형 참조

  • 컨텐츠 제공자는 표준 MIME 미디어 유형이나 사용자 지정 MIME 유형 문자열 또는 모두를 반환할 수 있음

MIME 유형의 형식

type/subtype
  • 예시로 잘 알려진 MIME 유형인 text/html의 경우 text 유형의 하위 유형인 html으로 볼 수 있음, 제공자가 이 유형의 URI를 반환한다면 해당 URI를 사용한 쿼리는 HTML 태그를 포함한 텍스트를 반환함

  • 공급업체별 MIME 유형이라고도 불리는 사용자 지정 MIME 유형 문자열의 경우 더 복잡한 유형과 하위 유형 값을 가짐, 유형 값은 항상 다음과 같음

여러 행을 가지는 유형 예시

vnd.android.cursor.dir

단일 행을 가지는 유형 예시

vnd.android.cursor.item
  • 하위 유형은 제공자에 의해 결정됨, 안드로이드에 내장된 제공자는 일반적으로 간단한 하위 유형을 가지고 있음

연락처 앱에서 전화 번호에 대한 행을 생성할 때의 MIME 유형 예시

vnd.android.cursor.item/phone_v2
  • 이 때 하위 유형의 값은 phone_v2

  • 다른 제공자를 개발하는 개발자는 테이블 이름과 제공자의 authority 를 기반으로 하위 유형의 패턴을 생성할 수 있음, 예시로 열차 시간표를 포함하는 제공자의 경우, 제공자의 authoritycom.example.trains 이고, Line1, Line2, Line3 테이블을 포함하고 있을 때를 생각하면 다음 예시와 같이 MIME 유형을 정의할 수 있을 것임

Line1 테이블의 컨텐츠 URI

content://com.example.trains/Line1

제공자가 반환하는 Line1 테이블의 MIME 유형

vnd.android.cursor.dir/vnd.example.line1

Line2 테이블의 5번째 행의 컨텐츠 URI

content://com.example.trains/Line2/5

제공자가 반환하는 해당 행의 MIME 유형

vnd.android.cursor.item/vnd.example.line2

대부분의 컨텐츠 제공자는 그들이 사용하는 MIME 유형에 대한 상수를 계약 클래스에 정의함, 예시로 연락처 제공자의 계약 클래스 ContactsContract.RawContacts 의 경우 CONTENT_ITEM_TYPE 에 단일 연락처 행의 MIME 유형이 정의되어 있음

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

0개의 댓글