페이지

2013년 6월 20일 목요일

Content Provider 기초

Content Provider는 데이터의 중앙 저장소에 대한 접근을 관리한다. Provider는 안드로이드 애플리케이션의 일종인데, 이것은 종종 데이터를 가지고 동작하는 자체 UI를 제공한다. 그러나, Content Provider는 주로 다른 애플리케이션에 의해 사용되는 목적인데, Provider 클라이언트 객체를 사용하는 Provider에 접근한다. 동시에, Provider와 Provider 클라이언트는 데이터에 대해서 프로세스 간 통신을 처리하고 데이터 접근을 안전하게 처리하는 일관된 표준 인터페이스를 제공한다.


이 주제에서는 아래의 기본 사항들을 서술한다:


  • Content Provider가 어떻게 동작하는가.
  • Content Provider에서 데이터를 가져오는 API.
  • Content Provider에 데이터를 추가하고 갱신하고 삭제하는 API.
  • Provider로 작업을 가능하게하는 다른 API 기능들.


개요

Content Provider는 관계형 데이터베이스에서 볼 수 있는 테이블들과 유시한 하나 이상의 테이블로써 외부의 애플리케이션에 데이터를 보여준다. 행(row)은 Provider가 모은 어떤 타입의 데이터의 인스턴스(instance)를 나타내고, 그 행의 각의 열(column)은 인스턴스에 대해 수집된 데이터의 개별 조각을 나타낸다.


예를 들어, 안드로이드 플랫폼에 내장된 Provider 중 하나인 사용자 사전은 사용자가 유지하고자 하는 비 표준 단어의 철자를 저장한다. 표 1은 Provider의 테이블에서 데이터가 어떻게 보여지는지를 설명하고 있다:


표 1: 샘플 사용자 사전 테이블
word
app id
frequency
locale
_ID
mapreduce
user1
100
en_US
1
precompiler
user14
200
fr_FR
2
applet
user2
225
fr_CA
3
const
user1
255
pt_BR
4
int
user5
100
en_UK
5


표 1에서, 각 행은 표준 사전에서 발견되지 않은 단어의 인스턴스를 나타낸다. 각 열은 어떤 단어의 데이터를 나타내는데, 예컨데 단어가 처음 발생한 로케일 등이다. 열 헤더는 Provider에 저장되는 열 이름이다. 행의 로케일을 참조하기 위해서는 그 행의 locale 열을 참조하면 된다. 이 Provider에서 _ID 열은 “기본 키(primary key)”의 역할을 하는데 Provider가 자동으로 관리한다.


:  Provider는 기본 키가 필수는 아니고, 그것이 있다면 기본 키의 열 이름으로 _ID를 사용해야만 하는 것도 아니다. 그러나 만약 ListView와 Provider의 데이터를 바인드하고 싶다면 열 이름 중 하나는 반드시 _ID여야 한다. 이 요구 사항은 Displaying query results 절에서 더 자세히 설명하고 있다.

Provider 접근

애플리케이션은 ContentResolver 클라이언트 객체를 가지고 Content Provider로부터 데이터에 접근한다. 이 객체는 Provider 객체에 동일하게 이름지어진 메소드를 호출하는 메소드들을 가지고 있는데, ContentProvider의 구체회된 서브클래스 중 하나의 인스턴스이다. ContentResolver 메소드는 영구적인 저장 공간의 기본적인 “CRUD”(create, retrieve, update, delete)를 제공한다.


클라이언트 애플리케이션 프로세스의 ContentResolver 객체와 Provider를 가지고 있는 애플리케이션의 ContentProvider 객체는 자동적으로 프로세스 간 통신을 처리한다. ContentProvider는 또한 데이터 저장소와 테이블로써의 외부 데이터 모습 사이에서 추상화 계층으로써의 역할을 한다.


주: Provider에 접근하기 위해, 애플리케이션은 보통 Manifest 파일에서 지정된 권한을 요청해야 한다. 이것은 Content Provider Permissions 절에서 상세히 설명하고 있다.


예를 들면, User Dictionary Provider에서 단어와 로케일 목록을 얻기 위해서, ContentResolver.query()를 호출한다. query() 메소드는 User Dictionary Provider에서 정의하고 있는 ContentProvider.query() 메소드를 호출한다. 아래의 코드는 ContentResolver.query()가 호출되는 것을 보여준다:




// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
    mProjection,                        // The columns to return for each row
    mSelectionClause                    // Selection criteria
    mSelectionArgs,                     // Selection criteria
    mSortOrder);                        // The sort order for the returned rows







표 2는 query(Uri,projection,selection,selectionArgs,sortOrder)의 인자가 SQL SELECT 구문과 어떻게 대응되는지 보여준다:


표 2: Query()와 SQL 쿼리 비교.
query() 인자
SELECT 키워드/매개변수
설명
Uri
FROM table_name
Uri는 table_name으로 이름이 붙은 Provider의 테이블에 매핑된다.
projection
col, col, col, ...
projection은 가져온 각 행에 포함되어야 할 열의 배열이다.
selection
WHERE col = value
selection은 선택 행에 대한 분류를 지정한다.
selectionArgs
(정확히 일치하는 것이 없다. 선택 인자는 선택 절의 ?를 대체한다)

sortOrder
ORDER BY col, col, ...
sortOrder는 Cursor로 리턴된 행이 나타내는 순서를 지정한다.


Content URIs
Content URI는 Provider의 데이터를 식별하는 URI이다. Content URI는 전체 Provider의 심볼릭 이름(그것의 authority)과 테이블을 가리키는 이름(path)를 포함한다. Provider의 테이블에 접근하기 위해 클라이언트 메소드를 호출할 때, 테이블의 Content URI는 인자들 중 하나이다.


앞에서 보았던 코드에서CONTENT_URI 상수는 “words” 사용자 사전의 테이블의 Content URI를 포함한다. ContentResolver 객체는 URI의 authority를 파싱하고 이것을 이미 알려진 Provider의 시스템 테이블에 대한 authority와 비교하여 Provider를 “분석”하기 위해 사용한다.


ContentProvider는 접근할 테이블을 선택하기 위해 Content URI의 경로 부분을 사용한다. Provider는 보통 외부에 노출된 각각의 테이블에 대한 path를 가지고 있다.


앞에서 본 코드에서, “words” 테이블의 전체 URI는 다음과 같다:


content://user_dictionary/words


user_dictionary 문자열은 Provider의 authroty이고, words 문자열은 테이블의 경로이다. content:// (scheme) 문자열은 항상 있어야 하고 이것이 Content URI라는 것을 식별하게 해준다.


많은 Provider들은 URI의 끝에 ID 값을 붙여서 테이블의 단일 행에 접근할 수 있게 해준다. 예를 들면, 사용자 사전으로부터 _ID가 4인 행을 가져오기 위해서 이러한 Content URI를 사용할 수 있다:



Uri singleUri = ContentUrls.withAppendedId(UserDictionary.Words.CONTENT_URI, 4);



종종 여러 행의 집합을 가져오고 그것들을 갱신하거나 삭제하고 싶을 때 id 값을 사용한다.


주: Uri와 Uri.Builder 클래스는 문자열로부터 잘 구성된 Uri 객체를 만들기 위한 편리한 메소드를 포함한다. ContentUris는 URI에 id 값을 붙이는 편리한 메소드를 포함한다. 앞에서 본 코드는 UserDictionary Content URI에 id를 붙이기 위해 withAppendedId() 메소드를 사용한다.


Provider에서 데이터 가져오기

이 절에서는 User Dictionary Provider를 예제로 사용해서 Provider에서 데이터를 가져오는 방법을 설명한다.


명확히 할 것은, 이 절에서 사용되는 코드는 “UI 스레드”에서 ContentResolver.query()를 호출한다는 것이다. 그러나 실제 코드에서는 분리된 스레드에서 비동기적으로 쿼리해야 한다. 이것을 하기 위한 한 가지 방법은 CursorLoader 클래스를 사용하는 것인데, 이것은 Loaders 가이드에서 상세히 다루고 있다. 또한, 이 문서의 코드는 단편일 뿐이다; 전체 애플리케이션을 보여주는 것은 아니다.


Provider에서 데이터를 가져오기 위해, 이러한 기본적인 단계를 따라야 한다:


1. Provider를 위한 읽기 접근 권한을 요청한다.
2. Provider에 쿼리를 보내는 코드를 정의한다.


읽기 접근 권한 요청하기

Provider에서 데이터를 가져오기 위해, 애플리케이션은 Provider를 위한 “읽기 접근 권한”이 필요하다. 이 권한은 실행 중에 요청할 수없다; 대신, <user-permission> 요소와 Provider에 의해 정의된 정확한 권한명을 사용해서 manifest 파일에 이 권한을 요청하도록 지정해야 한다. 이러한 요소들을 manifest 파일에 지정할 때, 애플리케이션을 위한 권한을 “요청”하는 효과가 있다. 사용자들이 애플리케이션을 설치할 때, 암묵적으로 이 요청을 승인한다.


사용할 Provider를 위한 읽기 접근 권한의 정확한 이름 뿐만 아니라 Provider에 의해 사용되는 다른 접근 권한의 이름을 찾기 위해서 Provider 문서를 참고하라.


Provider에 접근하는 권한의 역할은 Content Provider Permissions 절에서 상세히 설명하고 있다.


User Dictionary Provider는 manifest 파일에서 android.permission.READ_USER_DICTIONARY 권한을 정의하고 있기 때문에 Provider로부터 읽기를 원하는 애플리케이션은 이 권한을 요청해야 한다.


쿼리 구성하기

Provider에서 데이터를 가져오는 다음 단계는 쿼리를 구성하는 것이다. 이 첫 번째 코드는 User Dictionary Provider를 접근하기 위한 몇 개의 변수를 정의한다:



// A "projection" defines the columns that will be returned for each row
String[] mProjection =
{
    UserDictionary.Words._ID,    // Contract class constant for the _ID column name
    UserDictionary.Words.WORD,   // Contract class constant for the word column name
    UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};

 // Defines a string to contain the selection clause
String mSelectionClause = null;

 // Initializes an array to contain selection arguments
String[] mSelectionArgs = {""};




다음 단계의 코드는 User Dictionary Provider를 예제로 사용해서 ContentResolver.query()를 사용하는 법을 보여준다. Provider 클라이언트 쿼리는 SQL 쿼리와 비슷하고 이것은 리턴할 열의 집합, 선택한 항목의 집합, 정렬 순서를 포함한다.


쿼리가 리턴해야 하는 열의 집합은 projection이라고 부른다(mProjection 변수).


가져올 행을 가리키는 수식은 선택 절과 선택 인자로 구분된다. 선택 절은 논리, Boolean 수식과 열 이름, 그리고 값의 조합이다(mSelectionClause 변수). 만약 어떤 값을 대신해서 교체 가능한 매개변수인 ?를 지정한다면, 쿼리 메소드는 선택 인자 배열(mSelectionArgs)로부터 값을 가져온다.


다음 코드에서는, 사용자가 단어를 입력하지 않으면 선택 절은 null이 되고 쿼리는 Provider에서 모든 단어를 리턴한다. 만약 사용자가 단어를 입력한다면 선택 절은 UserDictionary.Words + “ = ?”가 되고 선택 인자 배열의 첫 번째 배열은 사용자가 입력한 단어가 된다.



/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] mSelectionArgs = {""};

 // Gets a word from the UI
mSearchString = mSearchWord.getText().toString();

 // Remember to insert code here to check for invalid or malicious input.

 // If the word is the empty string, gets everything
if (TextUtils.isEmpty(mSearchString)) {
    // Setting the selection clause to null will return all words
    mSelectionClause = null;
    mSelectionArgs[0] = "";

 } else {
    // Constructs a selection clause that matches the word that the user entered.
    mSelectionClause = UserDictionary.Words.WORD + " = ?";

     // Moves the user's input string to the selection arguments.
    mSelectionArgs[0] = mSearchString;

 }

 // Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    mProjection,                       // The columns to return for each row
    mSelectionClause                   // Either null, or the word the user entered
    mSelectionArgs,                    // Either empty, or the string the user entered
    mSortOrder);                       // The sort order for the returned rows

 // Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You may want to
     * call android.util.Log.e() to log this error.
     *
     */
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

     /*
     * Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
     * an error. You may want to offer the user the option to insert a new row, or re-type the
     * search term.
     */

 } else {
    // Insert code here to do something with the results

 }



이 쿼리는 다음 SQL 구문과 유사하다:


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


SQL 구문에서, 실제 열 이름은 정해진 클래스 상수 대신 사용된다.

악의적인 입력으로부터 보호하기

만약 Content Provider에 의해 관리되는 데이터는 SQL 데이터베이스에 있다면, Raw SQL 구문에 외부의 신뢰되지 않은 데이터를 포함하는 것이 SQL 인덱션을 야기할 수 있다.


이 선택 절을 생각해보자:



// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause =  "var = " + mUserInput;




이렇게 한다면, SQL 구문에 악의적인 SQL 구문을 연결하게 할 수 있다. 예를 들면, 사용자가 mUserInput에 var = nothing; DROP TABLE *; 을 mUserInput으로 입력하면 선택 절은 var = nothing; DROP TABLE *;가 된다. 이 선택 절이 SQL 구문으로 처리되기 때문에 이것은 Provider가 SQLite 데이터베이스의 모든 테이블을 삭제하게 만들 수 있다(Provider가 SQL 인젝션을 잡기 위해 설정하지 않았다면).


이 문제를 피하기 위해서, 교체 가능한 매개변수로 ?를 사용하는 선택 절과 선택 인자의 구분된 배열을 사용해야 한다. 이것을 할 때, 사용자 입력은 SQL 구문의 일부로 번역되도록 하지 말고 쿼리에 직접 연결되게 한다. 이것은 SQL 구문으로 처리되지 않기 때문에, 사용자 입력은 악의적인 SQL 인젝션을 할 수 없다. 사용자 입력을 포함시키기 위해 문자열 연결을 사용하는 대신 이러한 선택 절을 사용하라:



// Constructs a selection clause with a replaceable parameter
String mSelectionClause =  "var = ?";




선택 인자의 배열을 이렇게 설정하라:



// Defines an array to contain the selection arguments
String[] selectionArgs = {""};



선택 인자 배열의 값을 이렇게 넣어라:



// Sets the selection argument to the user's input
selectionArgs[0] = mUserInput;



교체 가능한 매개변수와 선택 인자 배열의 배열로써 사용하는 선택 절은 비록 Provider가 SQL 데이터베이스에 기반을 두고 있지 않아도 선택을 지정하는 나은 방법이다.


쿼리 결과 표시

ContentResolver.query() 클라이언트 메소드는 항상 쿼리의 선택 항목과 일치하는 행에 대한 쿼리의 프로젝션(projection)에 의해 지정된 열들을 포함하는 Cursor를 리턴한다. Cursor 객체는 이것이 가지고 있는 행과 열에 대한 무작위 접근을 제공한다. Cursor 메소드를 제공할 때, 결과에서 행들을 반복 처리하고 각 열의 데이터 타입을 결정하고 열에서 데이터를 가져오고 결과의 다른 속성들을 분석할 수 있다.어떤 Cursor 구현은 자동으로 Provider의 데이터가 변경될때 객체를 업데이트하거나 Cursor이 변경될 때 옵저버(observer) 객체에 있는 메소드를 실행하거나 둘 다 한다.


주: Provider는 쿼리를 만드는 객체의 특성에 따라서 열에 대한 접근을 제한할 수도 있다. 예를 들면, 연락처 Provider는 어댑터와 동기화하기 위해 일부 열에 대한 접근을 제한해서 이것은 액티비티나 서비스에 리턴하지 않을 것이다.


만약 선택 항목과 일치하는 행이 없으면 Provider는 Cursor.getCount()가 0인(빈 Cursor) Cursor 객체를 리턴한다.


만약 내부 에러가 발생하면 쿼리의 결과는 해당 Provider에 의존한다. null을 리턴하거나 Exception을 발생하게 할 수도 있다.


Cursor가 행의 “목록”이기 때문에 Cursor의 내용을 보여주기에 좋은 방법은 이것은 SimpleCursorAdapter를 통해 ListView와 연결하는 것이다.


아래의 코드는 앞선 코드에서 이어지는 것이다. 이 코드는 쿼리에 의해 가져온 Cursor를 가지고 있는 SimpleCursorAdapter 객체를 만들고 ListView를 위한 adpater로 설정한다.



// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

 // Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};

 // Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    mWordListColumns,                      // A string array of column names in the cursor
    mWordListItems,                        // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)

 // Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);



주: Cursor를 가진 ListView로 돌아가기 위해, Cursor는 _ID 이름이 붙은 열을 포함해야 한다. 이것 때문에 앞에서 본 쿼리는 비록 ListView가 이것을 표시하지 않지만 “words” 테이블의 _ID 열을 가져온다.


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

쿼리 결과를 간단히 보여주지 않고 다른 작업들을 위해 이것을 사용할 수 있다. 예를 들면, 사용자 사전에서 철자를 가져올 수 있고 그 다음에 다른 Provider에서 찾을 수 있다. 이것을 하기 위해서 Cursor의 행들을 반복한다:



// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

 /*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers may throw an Exception instead of returning null.
 */

 if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */
    while (mCursor.moveToNext()) {

         // Gets the value from the column.
        newWord = mCursor.getString(index);

         // Insert code here to process the retrieved word.

         ...

         // end of while loop
    }
} else {

     // Insert code here to report an error if the cursor is null or the provider threw an exception.
}



Cursor 구현은 객체로부터 다른 데이터 타입을 가져오기 위한 여러 “get” 메소드를 포함한다. 예를 들면, 앞선 코드에서 getString()을 사용한다. 열의 데이터타입을 가리키는 값을 리턴하는 getType() 메소드도 가지고 있다.


Content Provider 권한

Provider의 애플리케이션은 Provider의 데이터에 접근하기 위해 다른 애플리케이션이 가져야 하는 권한을 지정할 수 있다. 이러한 권한들은 애플리케이션이 어떤 데이터에 접근하려고 시도할 것인지 사용자가 알게 해준다. Provider의 요구에 따라, 다른 애플리케이션은 Provider에 접근하기 위해 필요한 권한을 요청한다. 최종 사용자는 애플리케이션을 설치할 때 요청되는 권한을 볼 수 있다.


만약 Provider의 애플리케이션이 어떠한 권한도 지정하지 않은다면 다른 애플리케이션들은 Provider의 데이터에 접근하지 못한다. 그러나, Provider의 애플리케이션에 있는 컴포넌트는 항상 지정된 권한에 상관없이 전체 읽기 쓰기 원한을 가지고 있다.


앞에서 언급한 것처럼, User Dictionary Provider는 android.permission.READ_USER_DICTIONARY 권한은 데이터를 가져오기 위해 필요하다. 데이터를 추가하고 갱신하고 삭제하기 위해서는 별도의 android.permission.WRITE_USER_DICTIONARY 권한을 가지고 있다.


Provider에 접근하기 위해 요구되는 권한을 얻기 위해서, 애플리케이션은 manifest 파일에 <uses-permission> 요소를 가지고 권한을 요청한다. Android Package Manager가 애플리케이션을 설치할 때, 사용자는 애플리케이션이 요청하는 모든 권한을 승인해야 한다. 만약 사용자가 모든 권한을 승인하면, Package Manager는 설치를 계속한다; 만약 사용자가 승인하지 않으면 Package Manager는 설치를 중단한다.


다음의 <uses-permission> 요소는 User Dictionary Provider에 읽기 접근을 요청한다.


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


Provider 접근에 대한 권한의 효과는 Security and Permissions 가이드에서 상세히 설명하고 있다.


데이터 추가, 갱신, 삭제

Provider에서 데이터를 가져오는 동일한 방식으로 데이터를 변경하기 위해 Provider 클라이언트와 Provider의 ContentProvider 사이의 상호 작용을 사용한다. ContentProvider의 해당 메소드에 인자를 넘겨서 ContentResolver의 메소드를 호출한다. Provider와 Provider 클라이언트는 자동으로 보안과 프로세스 간 통신을 처리한다.


데이터 추가

Provider에 데이터를 추가하기 위해 ContentResolver.insert() 메소드를 호출한다. 이 메소드는 Provider에 새로운 행을 추가하고 그 행의 컨텐츠 URI를 리턴한다. 이 코드는 User Dictionary Provider에 새 단어를 추가하는 방법이다.



// Defines a new Uri object that receives the result of the insertion
Uri mNewUri;

 //...

 // Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();

 /*
 * Sets the values of each column and inserts the word. The arguments to the "put"
 * method are "column name" and "value"
 */
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");

 mNewUri = getContentResolver().insert(
    UserDictionary.Word.CONTENT_URI,   // the user dictionary content URI
    mNewValues                          // the values to insert
);



새로운 행에 대한 데이터는 하나의 ContentValues 객체가 되고, 이것은 한 행의 Cursor와 유사한 형태이다. 이 객체의 열은 동일한 데이터 타입을 가질 필요가 없고 만약 모든 값을 지정하길 원하지 않는다면 ContentValues.putNull()을 사용해서 열에 null을 설정할 수 있다.


이 코드는 _ID 열을 추가하지 않는데, 왜냐하면 이 열은 자동으로 유지되기 때문이다. Provider는 추가되는 모든 행에 _ID의 유일한 값을 할당한다. Provider는 보통 이 값을 테이블의 기본키로 사용한다.


newUri로 리턴된 Content URI는 다음의 형태로 새로 추가된 행을 나타낸다.


content://user_dictionary/words/<id_value>


<id_value>는 새 행의 _ID의 컨텐트이다. 대부분의 Provider들은 자동으로 이러한 형태의 Content URI의 형태를 탐지할 수 있고 그 다음 해당하는 행에 요청된 연산을 수행한다.


리턴된 Uri로부터 _ID의 값을 얻기 위해서는 ContentUris.parseId()를 호출한다.


데이터 갱신

어떤 행을 갱신하기 위해서, 데이터를 추가할와 같이, 갱신된 값을 가진 ContentValues 객체를 사용하고, 선택 항목은 쿼리와 마찬가지로 사용한다. 사용하는 클라이언트 메소드는 ContentResolver.update()이다. 갱신하고 있는 열의 ContentValues 객체에 값을 추가할 필요가 있다. 만약 열의 값을 지우고 싶다면 이 값을 null로 설정하라.


아래의 코드는 “en” 언어로 로케일을 가지고 있는 모든 행의 로케일을 null로 바꾼다. 리턴 값은 업데이트 된 행의 숫자이다:



// Defines an object to contain the updated values
ContentValues mUpdateValues = new ContentValues();

 // Defines selection criteria for the rows you want to update
String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";
String[] mSelectionArgs = {"en_%"};

 // Defines a variable to contain the number of updated rows
int mRowsUpdated = 0;

 //...

 /*
 * Sets the updated value and updates the selected words.
 */
mUpdateValues.putNull(UserDictionary.Words.LOCALE);

 mRowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mUpdateValues                       // the columns to update
    mSelectionClause                    // the column to select on
    mSelectionArgs                      // the value to compare to
);



ContentResolver.update()를 호출할 때 사용자가 입력한 것에서 잘못된 것은 제거해야 한다. 이것에 대하 더 많은 것을 알려면 Protecting against malicious input 절을 참조하라.


데이터 삭제

행을 삭제하는 것은 행 데이터를 가져오는 것과 유사하다: 삭제하길 윈하는 행의 선택 항목을 지정하고 클라이언트 메소드는 삭제된 행의 개수를 리턴한다. 아래의 코드는 “user”와 일치하는 appid를 가진 행을 삭제한다. 이 메소드는 삭제된 행의 개수를 리턴한다.



// Defines selection criteria for the rows you want to delete
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"};

 // Defines a variable to contain the number of rows deleted
int mRowsDeleted = 0;

 //...

 // Deletes the words that match the selection criteria
mRowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mSelectionClause                    // the column to select on
    mSelectionArgs                      // the value to compare to
);



ContentResolver.update()를 호출할 때 사용자가 입력한 것에서 잘못된 것은 제거해야 한다. 이것에 대하 더 많은 것을 알려면 Protecting against malicious input 절을 참조하라.


Provider 데이터 타입

Content Provider는 여러 가지 데이터 타입을 제공할 수 있다. User Dictionary Provider는 오직 텍스트만 제공하지만, Provider는 다음 포맷을 제공할 수도 있다:


  • integer
  • long integer(long)
  • floating point
  • long floating point(double)


Provider가 제공하는 또 다른 데이터 타입은 64KB 바이트 배열로 구현된 Binary Large OBject(BLOB). Cursor 클래스의 “get” 메소드를 보면 사용 가능한 데이터 타입을 볼 수 있다.


Provider의 각 열의 데이터 타입은 일반적으로 그 문서에 나열되어있다. User Dictionary Provider의 데이터 타입은 그것의 계약 클래스에 대한 참조 문서에 나열되어 있다(계약 클래스는 Contract Classes 절에서 설명하고 있다). Cursor.getType()에 의해 데이터 타입을 정할 수 있다.


Provider는 또한 정의된 Content URI를 위한 MIME 데이터 타입 정보를 관리한다. 애플리케이션이 Provider가 제공하는 데이터를 처리할 수 있는지 알기 위해서, 또는 MIME 타입에 기반하여 처리한 것의 타입을 선택하기 위해서 MIME 타입 정보를 사용할 수 있다. 복잡한 데이터 구조나 파일을 가지고 있는 Provider로 작업할 때 MIME 타입이 필요하다. 예를 들면, 연락처 Provider의 ContactsContract.Data 테이블은 각 행에 저장된 연락처 정보의 타입에 MIME 타입을 붙이기 위해 사용한다. Content URI에 해당하는 MIME 타입을 얻기 위해서, ContentResolver.getType()을 호출해야 한다.


MIME Type Reference 절에서 표준, 커스텀 MIME 타입의 문법을 정의한다.


Provider 접근의 대체 형태들

Provider 접근의 세 가지 대체 형태가 애플리케이션 개발에서 중요하다.


  • 일괄 접근: ContentProviderOperation에 있는 메소드를 호출해서 일괄 접근을 만들 수 있고 ContentResolver.applyBatch()를 가지고 그것을 적용할 수 있다.
  • 비동기 쿼리: 쿼리는 별도의 스레드에서 처리해야 한다. 이것을 하기 위한 한 가지 방법은 CursorLoader 객체를 사용하는 것이다. Loaders 가이드에 있는 예제는 이것을 하는 방법을 설명하고 있다.
  • Intent를 통한 데이터 접근: 비록 Provider에 직접 Intent를 보낼 수 없지만, Provider의 애플리케이션에 Intent를 보낼 수 있는데, 이것은 Provider의 데이터를 수정하기 위해 가장 잘 갖춰진 것이다.


Intent를 통한 일괄 접근과 변경은 아래의 절에서 설명된다.


일괄 접근

Provider에 대한 일괄 접근은 많은 수의 행을 추가하거나 여러 테이블에 동일한 메소드를 호출하거나 트랜잭션(원자적 연산)으로써 프로세스 경계를 가로지르는 연산의 집합을 수행하기 위해 행을 추가할 때 유용하다.


“일괄 처리 모드”에서 Provider에 접근하기 위해, ContentProviderOperation의 배열을 만들고 ContentResolver.applyBatch()를 가지고 Content Provider에 그것들을 보낸다. 이것은 다른 테이블에 대해서 배열 안의 각 ContentProviderOperation 객체가 작업할 수 있게 해준다. ContentResolver.applyBatch()를 호출하면 결과 배열을 리턴한다.


ContactsContract.RawContacts 계약 클래스의 상세 설명은 일괄 추가를 설명하는 코드를 포함한다. Contact Manager 샘플 애플리케이션은 ContactAdder.java 소스 파일은 일괄 접근의 예제를 포함하고 있다.


Intent를 통한 데이터 접근

Intent는 Content Provider에 간접적으로 접근할 수 있다. 비록 애플리케이션이 접근 권한을 가지고 있지 않더라도 권한을 가진 애플리케이션에서 인텐트 결과를 가져오거나 권한을 가지고 있는 애플리케이션을 활성화하고 사용자가 그 안에서 작업하도록 하여 사용자는 Provider의 데이터에 접근하게 해준다.


임시 권한으로 접근

비록 적절한 접근 권한을 가지고 있지 않아도 권한을 가지고 있는 애플리케이션에 Intent를 보내고 “URI” 권한을 가지고 있는 Intent를 결과로 받으면 Content Provider에 있는 데이터에 접근할 수 있다. 이것들은 그것을 받은 액티비티가 완료될때까지 특정 Content URI가 유지되는 권한이다. 영구적인 권한을 가진 애플리케이션은 결과 Intent에 있는 플래그를 설정해서 임시 권한을 허용해준다:


  • 읽기 권한: FLAG_GRANT_READ_URI_PERMISSION
  • 쓰기 권한: FLAG_GRANT_WRITE_URI_PERMISSION


주: 이러한 플래그들은 Content UI에 포함된 authority를 가진 Provider에 접근해서 읽거나 쓰도록 하지는 않는다. 접근은 오직 URI 자체로만 된다.


<provider> 요소의 android:grantUriPermission 속성이나 <provider>의 하위 요소인 <grant-uri-permission>을 사용해서 Provider는 manifest에 Content URI를 위해 URI 권한을 정의한다. URI 권한 메카니즘은 “URI Permissions” 절에서 Security and Permission 가이드에서 상세히 설명하고 있다.


예를 들면, READ_CONTACTS 권한을 가지고 있지 않아도 Contacts Provider에 있는 연락처 정보를 가져올 수 있다. 사람들의 생일이 되면 연락처로 e-greeting을 보내는 애플리케이션에서 이것을 원할 수도 있다. 사용자의 연락처와 그들의 모든 정보를 접근하게 해주는 READ_CONTACTS를 요청하는 대신, 애플리케이션에 의해 사용되는 연락처를 사용자가 제어하도록 하는 것을 선호한다. 이것을 하기 위해 아래의 처리를 사용한다:


애플리케이션은 startActivityForResult() 메소드를 사용해서 ACTION_PICK 액션과 “연락처” MIME 타입을 CONTENT_ITEM_TYPE을 가지고 있는 Intent를 보낸다.
이 Intent가 People 앱의 “선택” 액티비티에 대한 Intent와 일치하기 때문에 액티비티는 포그라운드로 올 것이다.
선택 액티비티에서, 사용자는 갱신할 연락처를 선택한다. 이렇게 하면 선택 액티비티는 애플리케이션으로 돌려주기 위한 Intent를 설정하기 위한 setResult(resultcode, intent)를 호출한다. Intent는 선택된 사용자의 연락처에 대한 Content URI와 “추가” 플래그인 FLAG_GRANT_READ_URI_PERMISSION을 포함한다. 이러한 플래그는 앱이 Content URI가 가리키는 연락처의 데이터를 읽도록 권한을 허용한다. 그 다음에 선택 액티비티는 애플리케이션으로 제어권을 돌려주기 위해 finish()를 호출한다.
액티비티가 포그라운드로 돌아오고 시스템은 액티비티의 onActivityResult() 메소드를 호출한다. 이 메소드는 People 앱에서 선택 액티비티에 의해 만들어진 Intent를 결과로 받는다.
결과 Intent로부터 Content URI를 가지고, 비록 manifest에서 Provider에 대한 영구적인 읽기 권한을 요청하지 않는다고 해도 Contacts Provider에서 연락터 데이터를 읽을 수 있다. 그 다음에 사람들의 생일 정보, email 주소를 얻어서 e-greeting을 보낼 수 있다.


또 다른 애플리케이션 사용

접근 권한을 가지고 있지 않으면서 데이터를 수정할 수 있도록 해주는 쉬운 방법은 권한을 가진 애플리케이션을 활성화하고 거기에서 작업을 수행하게 하는 것이다.


예를 들면, 일정표 애플리케이션은 ACTION_INSERT Intent를 받는데, 이것은 애플리케이션의 추가 UI를 활성화하게 해준다.이 Intent에 “extras” 데이터를 넘길 수 있는데, 이것은 애플리케이션이 UI에 미리 배치하는데 사용한다. 반복되는 이벤트는 복잡한 문법을 가지고 있기 때문에, Calendar Provider에 이벤트를 추가하는 좋은 방법은 ACTION_INSERT를 가지고 Calendar 앱을 활성화하는 것이고 사용자가 이벤트를 추가하게 한다.


계약 클래스

계약 클래스는 Content URI, 열 이름, Intent 동작, 그리고 Content Provider 의 다른 기능으로 작업하는 애플리케이션을 돕은 상수들을 정의한다. 계약 클래스는 Provider에 자동으로 포함되지는 않는다; Provider의 개발자는 이것들을 정의하고 다른 개발자가 사용할 수 있게 만들어야 한다. 안드로이드 플랫폼에 포함된 많은 Provider들이 android.provider 패키지에 해당하는 계약 클래스를 가지고 있다.


예를 들어, User Dictionary Provider는 Content URI와 열 이름 상수를 포함하고 있는 UserDictionary 계약 클래스를 가지고 있다. "words" 테이블의 Content URI는 UserDictionary.Words.CONTENT_URI 상수로 정의되어 있다. UserDictionary.Words 클래스는 이 가이드의 예제 코드에서 사용된 열 이름 상수도 포함하고 있다. 예를 들면 쿼리 Projection은 다음과 같이 정의될 수 있다:



String[] mProjection =
{
    UserDictionary.Words._ID,
    UserDictionary.Words.WORD,
    UserDictionary.Words.LOCALE
};



또 다른 계약 클래스는 Contacts Provider를 위한 ContactsContract이다. 이 클래스의 참조 문서는 예제 코드를 포함하고 있다. 이거의 서브 클래스 중 하나는 ContactsContract.Intents.Insert인데 Intent와 Intent 데이터를 위한 상수를 포함하고 있는 계약 클래스이다.


MIME 타입 참조

Content Provider는 표준 MIME 미디어 타입 또는 커스텀 MIME 타입 문자열 또는 둘 다 리턴할 수 있다.


MIME 타입은 일정한 형식을 가지고 있다.


type/subtype


예를 들면, 잘 알려진 MIME 타입인 text/html은 text 타입이고 html 서브 타입이다. 만약 Provider가 URI에 대해 이런 타입을 리턴한다면 이것은 HTML 태그를 가지고 있는 텍스트를 리턴할 URI를 사용하는 쿼리라는 의미이다.


커스텀 MIME 타입 문자열은 "vendor-specific" MIME 타입이라고도 하는데, 조금 더 복잡한 타입과 서브 타입 값을 가지고 있다. 타입 값은 항상 다음과 같은데


vnd.android.cursor.dir


이것과 같이 여러 행에 대한 타입이거나


vnd.android.cursor.item


과 같이 하나의 행에 대한 타입이다.


서브 타입은 Provider-specific이다. 안드로이드에 내장된 Provider는 보통 단순한 서브 타입을 가지고 있다. 예를 들면, 연락처 애플리케이션이 전화 번호를 하나 만들때, 그 행에 대한 MIME 타입은 다음과 같이 정해진다:


vnd.android.cursor.item/phone_v2


서브 타입 값이 단지 phone_v2라는 것에 주목하자.


다른 Provider 개발자들은 자신들만의 서브 타입 패턴을 Provider의 authority와 테이블 이름을 가지고 만들 수 있다. 예를 들어, 열차 시간표를 가지고 있는 Provider를 생각해보자. Provider의 authority는 com.example.trains이고, 이것은 Line1, Line2, Line3의 시간표를  가지고 있다. 다음의 Line1에 대한 Content URI의 응답에서


content://com.example.trains/Line1


Provider는 MIME 타입을 리턴한다.


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


다음의 Line2에서 5행에 대한 Content URI의 응답에서


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


Provider는 MIME 타입을 리턴한다.


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


대부분의 Content Provider는 그들이 사용할 MIME 타입을 위해 계약 클래스 상수를 정의한다. Contacts Provider 계약 클래스인 ContactsContract.RawContacts는 단일 raw contact 행의 MIME 타입을 위해 CONTENT_ITEM_TYPE 상수를 정의한다.


단일 행에 대한 Content URI는 Content URIs 절에서 설명하고 있다.