콘텐츠 제공자는 중앙 저장소로의 데이터 액세스를 관리합니다. 제공자는 Android 애플리케이션의 일부이며, 대개 데이터 작업을 위한 고유의 UI를 제공합니다. 그러나 콘텐츠 제공자는 주로 애플리케이션. 이들은 제공자 클라이언트 객체를 사용하여 제공자에 액세스합니다. 제공업체 클라이언트 및 공급자 클라이언트는 데이터에 대한 일관성 있는 표준 인터페이스를 제공합니다. 프로세스 간 통신 및 안전한 데이터 액세스입니다.
일반적으로 다음과 같은 두 가지 시나리오 중 하나에서 콘텐츠 제공자와 작업합니다. 다른 애플리케이션의 기존 콘텐츠 제공자에 액세스하거나 다른 애플리케이션과 데이터를 공유할 수 있습니다.
이 페이지 에서는 기존 콘텐츠 제공업체와 함께 하는 작업의 기본 사항을 다룹니다. 앱 콘텐츠 구현에 대해 알아보기 위해 자세히 알아보려면 <ph type="x-smartling-placeholder"></ph> 콘텐츠 제공자 만들기
이 주제에서는 다음의 내용을 설명합니다.
- 콘텐츠 제공자의 작동 원리
- 콘텐츠 제공자에서 데이터를 검색할 때 사용하는 API
- 콘텐츠 제공자 내의 데이터를 삽입, 업데이트 및 삭제하는 데 사용하는 API
- 제공자를 다루는 데 도움이 되는 기타 API 기능
개요
콘텐츠 제공자는 외부 애플리케이션에 데이터를 표시하며, 이때 데이터는 관계형 데이터베이스에서 찾을 수 있는 테이블과 유사한 하나 이상의 테이블로 표시됩니다. 행은 제공자가 수집하는 특정 데이터 유형의 인스턴스를 나타내고, 행의 각 열은 인스턴스에 대해 수집된 개별 데이터를 나타냅니다.
콘텐츠 제공자는 다양한 API와 구성요소를 제공합니다. 그림 1과 같이 여기에는 다음이 포함됩니다.
- 다른 애플리케이션과 내 애플리케이션 데이터에 대한 액세스 공유
- 위젯에 데이터 전송
SearchRecentSuggestionsProvider
를 사용하여 검색 프레임워크를 통해 애플리케이션의 맞춤 추천 검색어 반환AbstractThreadedSyncAdapter
를 구현하여 서버와 애플리케이션 데이터 동기화CursorLoader
를 사용하여 UI에서 데이터 로드
![콘텐츠 제공자와 기타 구성요소 간의 관계](https://developer.android.google.cn/static/guide/topics/providers/images/content-provider-tech-stack.png?authuser=9&hl=ko)
그림 1. 콘텐츠 제공자와 기타 구성요소 간의 관계
제공업체 액세스
콘텐츠 제공자 내의 데이터에 액세스하고자 하는 경우, 애플리케이션의 Context
에 있는 ContentResolver
객체를 사용하여 클라이언트로서 제공자와 통신을 주고받으면 됩니다. ContentResolver
객체가 제공자 객체와 통신하며, 이 객체는 ContentProvider
를 구현하는 클래스의 인스턴스입니다.
제공자 객체가 클라이언트로부터 데이터 요청을 받아 요청된 작업을 실행하고 결과를 반환합니다. 이 객체에는 제공자 객체에서 동일한 이름의 메서드를 호출하는 메서드가 있습니다.
ContentProvider
의 구체적인 서브클래스 중 하나의 인스턴스입니다. 이
ContentResolver
메서드는
'CRUD' (생성, 검색, 업데이트, 삭제) 기능을 제공합니다.
UI에서 ContentProvider
에 액세스하기 위한 일반적인 패턴에서는 CursorLoader
를 사용하여 백그라운드에서 비동기식 쿼리를 실행합니다. 이
UI의 Activity
또는 Fragment
가
CursorLoader
를 쿼리에 추가하여
ContentResolver
를 사용하여 ContentProvider
.
이렇게 하면 쿼리가 실행되는 동안 사용자가 UI를 계속 사용할 수 있습니다. 이 패턴에는 그림 2에서와 같이 기본 저장소 메커니즘과 함께 여러 가지 다른 객체의 상호작용이 포함됩니다.
![콘텐츠 제공자, 기타 클래스 및 저장소 간의 상호작용](https://developer.android.google.cn/static/guide/topics/providers/images/content-provider-interaction.png?authuser=9&hl=ko)
그림 2. ContentProvider
, 다른 클래스, 스토리지 간의 상호작용
참고: 제공자에 액세스하려면 일반적으로 애플리케이션이 제공자의 매니페스트 파일에 있는 특정 권한을 요청해야 합니다. 이러한 개발 패턴은 콘텐츠 제공자 권한 섹션으로 이동하세요.
Android 플랫폼에 내장된 제공자 중 하나는 사용자 사전 제공자이며, 이 제공자는 는 사용자가 보관하고자 하는 비표준 단어를 저장합니다. 표 1은 이 제공자의 테이블에서 데이터가 어떤 형태를 띨 수 있는지를 보여줍니다.
표 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에서 각 행은
쉽게 찾을 수 있습니다. 각 열은 해당 단어에 대한 데이터를 나타냅니다. 예를 들면
처음 발생한 언어입니다. 열 헤더는 제공자에 저장된 열 이름입니다. 예를 들어 행의 언어를 참조하려면 locale
열을 참조합니다. 대상
이 공급자의 경우 _ID
열은 기본 키 열 역할을 하며
자동으로 유지관리합니다
사용자 사전 제공자로부터 단어와 그에 해당하는 언어 목록을 가져오려면 ContentResolver.query()
를 호출합니다.
query()
메서드는 사용자 사전 제공자가 정의한 ContentProvider.query()
메서드를 호출합니다. 다음 코드는 ContentResolver.query()
호출을 보여줍니다.
Kotlin
// Queries the UserDictionary and returns results cursor = contentResolver.query( UserDictionary.Words.CONTENT_URI, // The content URI of the words table projection, // The columns to return for each row selectionClause, // Selection criteria selectionArgs.toTypedArray(), // Selection criteria sortOrder // The sort order for the returned rows )
자바
// Queries the UserDictionary and returns results cursor = getContentResolver().query( UserDictionary.Words.CONTENT_URI, // The content URI of the words table projection, // The columns to return for each row selectionClause, // Selection criteria selectionArgs, // Selection criteria sortOrder); // 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라는 제공자의 테이블에 매핑됩니다. |
projection |
col,col,col,... |
projection 는 각 행에 포함된 열의 배열입니다.
있습니다.
|
selection |
WHERE col = value |
selection 은 행을 선택하는 기준을 지정합니다. |
selectionArgs |
정확히 일치하는 항목은 없습니다. 선택 인수는 다음의 ? 자리표시자를 대체합니다.
선택 절을 사용합니다.
|
|
sortOrder |
ORDER BY col,col,... |
sortOrder 는 반환된 Cursor 내에 행이 나타나는 순서를 지정합니다. |
콘텐츠 URI
콘텐츠 URI는 제공자에서 데이터를 식별하는 URI입니다. 콘텐츠 URI 전체 제공업체의 상징적인 이름(권한)과 테이블을 가리키는 이름(경로) 제공자 내의 테이블에 액세스하기 위해 클라이언트 메서드를 호출하는 경우, 테이블의 콘텐츠 URI는 인수 중 하나가 됩니다.
앞의 코드 줄에서는 상수가
CONTENT_URI
에는 다음과 같은 콘텐츠 URI가 포함됩니다.
사용자 사전 제공자의 Words
테이블. ContentResolver
객체는 URI의 권한을 파싱하고 이를 사용하여
알려진 제공자의 시스템 테이블과 권한을 비교합니다. 이
그러면 ContentResolver
가 쿼리 인수를 올바른
제공업체
ContentProvider
는 콘텐츠 URI의 경로 부분을 사용하여 액세스할 테이블을 선택합니다. 일반적으로 제공자에는 제공자가 노출하는 테이블마다 경로가 있습니다.
이전 코드 줄에서 Words
테이블의 전체 URI는 다음과 같습니다.
content://user_dictionary/words
content://
문자열은 항상 존재하는 스키마입니다. 를 콘텐츠 URI로 식별합니다.user_dictionary
문자열은 제공자의 권한입니다.words
문자열은 테이블의 경로입니다.
많은 제공자가 ID 값을 추가하여 테이블의 단일 행에 액세스할 수 있도록 허용합니다.
가 추가됩니다. 예를 들어 _ID
이 다음과 같은 행을 검색하려면
사용자 사전 제공업체의 4
에서 다음 콘텐츠 URI를 사용할 수 있습니다.
Kotlin
val singleUri: Uri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4)
자바
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
행 세트를 검색한 다음 업데이트 또는 삭제하고자 할 때 ID 값을 사용하는 경우가 많습니다. 둘 중 하나입니다
참고: Uri
및 Uri.Builder
클래스에는 문자열에서 올바른 형식의 URI 객체를 구성하기 위한 편의 메서드가 포함되어 있습니다. 이
ContentUris
클래스에는 ID 값을 추가하는 편의 메서드가 포함되어 있습니다.
생성하는 것입니다 이전 스니펫은 withAppendedId()
를 사용하여 사용자 사전 제공자 콘텐츠 URI에 ID를 추가합니다.
제공업체에서 데이터 검색
이 섹션에서는 사용자 사전 제공자를 예시로 사용하여 제공자에서 데이터를 검색하는 방법을 설명합니다.
명확성을 위해 이 섹션의 코드 스니펫은
UI 스레드의 ContentResolver.query()
포함
하지만 실제 코드는 별도의 스레드에서 비동기식으로 쿼리를 수행합니다. 다음과 같은 작업을 할 수 있습니다.
CursorLoader
클래스를 사용합니다.
자세한 내용은
로더 가이드를 참조하세요. 또한 코드 줄은 스니펫입니다. 전체
애플리케이션입니다.
제공자에서 데이터를 검색하려면 다음과 같은 기본 단계를 따르세요.
- 제공자에 대한 읽기 액세스 권한을 요청합니다.
- 제공자에게 쿼리를 보내는 코드를 정의합니다.
읽기 액세스 권한 요청
제공자에서 데이터를 검색하려면 애플리케이션에
제공업체 런타임 시 이 권한을 요청할 수 없습니다. 대신
매니페스트에 이 권한이 필요하며,
<uses-permission>
요소 및
제공업체
매니페스트에서 이 요소를 지정하면 권한을 부여해야 합니다. 사용자가 애플리케이션을 설치하면 이 요청을 암시적으로 허용하게 됩니다.
사용 중인 제공자에 대한 읽기 권한의 정확한 이름과 해당 제공자가 사용하는 다른 액세스 권한의 이름을 찾아보려면 제공자의 문서를 살펴보세요.
제공자 액세스에서 권한의 역할은 콘텐츠 제공자 권한 섹션으로 이동하세요.
사용자 사전 제공자는 android.permission.READ_USER_DICTIONARY
권한을 매니페스트 파일에 정의하므로 제공자에서 읽기 작업을 하고자 하는 애플리케이션은 반드시 이 권한을 요청해야 합니다.
쿼리 구성
제공자에서 데이터를 검색할 때 다음 단계는 쿼리를 구성하는 것입니다. 다음 스니펫 는 사용자 사전 제공자에 액세스하기 위한 몇 가지 변수를 정의합니다.
Kotlin
// A "projection" defines the columns that are returned for each row private val mProjection: Array<String> = arrayOf( 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 private var selectionClause: String? = null // Declares an array to contain selection arguments private lateinit var selectionArgs: Array<String>
자바
// A "projection" defines the columns that are 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 selectionClause = null; // Initializes an array to contain selection arguments String[] selectionArgs = {""};
다음 스니펫은 사용자 사전 제공자를 예시로 사용하여 ContentResolver.query()
를 사용하는 방법을 보여줍니다. 제공자 클라이언트 쿼리는 SQL 쿼리와 비슷하며,
선택 기준 집합 및 정렬 순서를 사용합니다.
쿼리가 반환하는 열 집합을 프로젝션이라고 합니다.
변수는 mProjection
입니다.
검색할 행을 나타내는 표현식은 선택 절과 선택 인수로 분할되어 있습니다. 선택 절은 논리 표현식과 불리언 표현식의 조합입니다.
열 이름, 값입니다. 변수는 mSelectionClause
입니다. 이
대체 가능한 매개변수 ?
가 있으면 쿼리 메서드가 값을 검색합니다.
선택 인수 배열에서 추출해야 합니다. 이 배열은 mSelectionArgs
변수입니다.
다음 스니펫에서 사용자가 단어를 입력하지 않으면 선택 절이
null
이면 쿼리가 제공자의 모든 단어를 반환합니다. 사용자가 단어를 입력하면 선택 절은 UserDictionary.Words.WORD + " = ?"
로 설정되며 선택 인수의 첫 번째 요소가 사용자가 입력한 단어로 설정됩니다.
Kotlin
/* * This declares a String array to contain the selection arguments. */ private lateinit var selectionArgs: Array<String> // Gets a word from the UI searchString = searchWord.text.toString() // Insert code here to check for invalid or malicious input // If the word is the empty string, gets everything selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let { selectionClause = "${UserDictionary.Words.WORD} = ?" arrayOf(it) } ?: run { selectionClause = null emptyArray<String>() } // Does a query against the table and returns a Cursor object mCursor = contentResolver.query( UserDictionary.Words.CONTENT_URI, // The content URI of the words table projection, // The columns to return for each row selectionClause, // Either null or the word the user entered selectionArgs, // Either empty or the string the user entered sortOrder // The sort order for the returned rows ) // Some providers return null if an error occurs, others throw an exception when (mCursor?.count) { null -> { /* * Insert code here to handle the error. Be sure not to use the cursor! * You might want to call android.util.Log.e() to log this error. */ } 0 -> { /* * Insert code here to notify the user that the search is unsuccessful. This isn't * necessarily an error. You might 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 } }
자바
/* * This defines a one-element String array to contain the selection argument. */ String[] selectionArgs = {""}; // Gets a word from the UI searchString = searchWord.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(searchString)) { // Setting the selection clause to null returns all words selectionClause = null; selectionArgs[0] = ""; } else { // Constructs a selection clause that matches the word that the user entered selectionClause = UserDictionary.Words.WORD + " = ?"; // Moves the user's input string to the selection arguments selectionArgs[0] = searchString; } // 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 projection, // The columns to return for each row selectionClause, // Either null or the word the user entered selectionArgs, // Either empty or the string the user entered sortOrder); // 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 can * 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 is unsuccessful. This isn't necessarily * an error. You can 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 문에서는 계약 클래스 상수 대신 실제 열 이름을 사용했습니다.
악의적인 입력으로부터 보호
콘텐츠 제공자가 관리하는 데이터가 SQL 데이터베이스에 있는 경우(외부의 신뢰할 수 없는 데이터 포함) 원시 SQL 문으로 변환할 수 있습니다.
다음 선택 절을 생각해 보세요.
Kotlin
// Constructs a selection clause by concatenating the user's input to the column name var selectionClause = "var = $mUserInput"
자바
// Constructs a selection clause by concatenating the user's input to the column name String selectionClause = "var = " + userInput;
이렇게 하면 사용자가 잠재적으로 악의적인 SQL을 SQL 문에 연결할 수 있습니다.
예를 들어 사용자는 'nothing; 테이블 삭제 *; mUserInput
,
선택 절 var = nothing; DROP TABLE *;
이 생성됩니다.
선택 절이 SQL 문으로 취급되므로 공급자가 모든 기본 SQLite 데이터베이스의 테이블 SQL 삽입 시도
이 문제를 피하려면 ?
를 대체 가능한 매개변수로 사용하는 선택 절과 별도의 선택 인수 배열을 사용하면 됩니다. 이렇게 하면
SQL 문의 일부로 해석되지 않고 쿼리에 직접 바인딩됩니다.
SQL로 취급되지 않기 때문에 사용자 입력이 악의적인 SQL을 주입할 수 없습니다. 사용자 입력을 포함하기 위해 문자열을 연결하는 대신 다음 선택 절을 사용하세요.
Kotlin
// Constructs a selection clause with a replaceable parameter var selectionClause = "var = ?"
자바
// Constructs a selection clause with a replaceable parameter String selectionClause = "var = ?";
그리고 다음과 같이 선택 인수를 배열하세요.
Kotlin
// Defines a mutable list to contain the selection arguments var selectionArgs: MutableList<String> = mutableListOf()
자바
// Defines an array to contain the selection arguments String[] selectionArgs = {""};
선택 인수 배열에 값을 입력할 때는 이렇게 합니다.
Kotlin
// Adds the user's input to the selection argument selectionArgs += userInput
자바
// Sets the selection argument to the user's input selectionArgs[0] = userInput;
?
를 대체 가능한 매개변수로 사용하는 선택 절과
선택 인수 배열은 선택 항목을 지정하는 데 선호되는 방법입니다. 이는 제공자가
SQL 데이터베이스를 기반으로 합니다
쿼리 결과 표시
ContentResolver.query()
클라이언트 메서드는 언제나 쿼리 선택 기준과 일치하는 행에 대해 쿼리 프로젝션이 지정한 열을 포함하는 Cursor
를 반환합니다. Cursor
객체가 자신이 포함한 행과 열에 무작위 읽기 액세스를 제공합니다.
Cursor
메서드를 사용하면
결과를 확인하고, 각 열의 데이터 유형을 결정하고, 열에서 데이터를 가져오고,
결과 속성입니다.
일부 Cursor
구현은 자동으로
제공자의 데이터가 변경될 때 객체 업데이트, 관찰자 객체에서 메서드를 트리거합니다.
Cursor
가 변경될 때, 또는 둘 다 변경될 때.
참고: 제공업체는 열의 특성에 따라 열에 대한 액세스를 제한할 수 있습니다. 객체를 반환합니다. 예를 들어 연락처 제공자는 일부 열에 대한 액세스를 동기화 어댑터를 사용하여 활동 또는 서비스에 반환하지 않습니다.
선택 기준과 일치하는 행이 없으면 제공자는
Cursor
객체를 반환합니다.
Cursor.getCount()
:
0: 빈 커서
내부 오류가 발생하는 경우, 쿼리 결과는 특정 제공자에 따라 달라집니다. 하지만
null
를 반환하거나 Exception
을 발생시킬 수 있습니다.
Cursor
는 행 목록이므로
Cursor
의 콘텐츠는 ListView
에 연결하는 것입니다.
SimpleCursorAdapter
를 사용합니다.
다음 스니펫은 이전 스니펫의 코드에서 계속된 것입니다. Kubernetes는
Cursor
를 포함하는 SimpleCursorAdapter
객체
이 객체를
ListView
입니다.
Kotlin
// Defines a list of columns to retrieve from the Cursor and load into an output row val wordListColumns : Array<String> = arrayOf( 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 receive the Cursor columns for each row val wordListItems = intArrayOf(R.id.dictWord, R.id.locale) // Creates a new SimpleCursorAdapter cursorAdapter = SimpleCursorAdapter( applicationContext, // The application's Context object R.layout.wordlistrow, // A layout in XML for one row in the ListView mCursor, // The result from the query wordListColumns, // A string array of column names in the cursor wordListItems, // An integer array of view IDs in the row layout 0 // Flags (usually none are needed) ) // Sets the adapter for the ListView wordList.setAdapter(cursorAdapter)
자바
// Defines a list of columns to retrieve from the Cursor and load into an output row String[] wordListColumns = { 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 receive the Cursor columns for each row int[] wordListItems = { R.id.dictWord, R.id.locale}; // Creates a new SimpleCursorAdapter cursorAdapter = 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 wordListColumns, // A string array of column names in the cursor wordListItems, // An integer array of view IDs in the row layout 0); // Flags (usually none are needed) // Sets the adapter for the ListView wordList.setAdapter(cursorAdapter);
참고: ListView
를 Cursor
와 함께 사용하려면 커서에 _ID
라는 열이 포함되어야 합니다.
따라서 이전에 표시된 쿼리는_ID
Words
테이블은 ListView
에서 표시하지 않더라도 마찬가지입니다.
이 제한사항은 또한 대부분의 제공업체에 각 네임스페이스에 대한 _ID
열이 있는 이유를 설명해 줍니다.
관리됩니다.
쿼리 결과에서 데이터 가져오기
쿼리 결과를 표시하는 것 외에도 다른 작업에 사용할 수 있습니다. 대상
예를 들어 사용자 사전 제공자에서 철자를 검색한 다음
다른 제공업체도 있습니다. 이렇게 하려면 다음 예와 같이 Cursor
의 행을 반복합니다.
Kotlin
/* * Only executes if the cursor is valid. The User Dictionary Provider returns null if * an internal error occurs. Other providers might throw an Exception instead of returning null. */ mCursor?.apply { // Determine the column index of the column named "word" val index: Int = getColumnIndex(UserDictionary.Words.WORD) /* * 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 get an * exception. */ while (moveToNext()) { // Gets the value from the column newWord = getString(index) // Insert code here to process the retrieved word ... // End of while loop } }
자바
// 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 might 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 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()
메서드도 있습니다.
콘텐츠 제공자 권한
제공자의 애플리케이션은 다른 애플리케이션이 반드시 가져야 하는 권한을 지정할 수 있습니다. 제공자의 데이터에 액세스할 수 없습니다. 이러한 권한을 통해 사용자는 애플리케이션이 액세스하려고 시도할 때. 다른 애플리케이션은 제공자의 요구사항을 근거로 해당 제공자에 액세스하기 위해 필요한 권한을 요청합니다. 최종 사용자는 애플리케이션을 설치할 때 요청된 권한을 보게 됩니다.
제공자의 애플리케이션이 아무 권한도 지정하지 않은 경우, 다른 애플리케이션에는 아무런 권한도 없습니다. 액세스 권한을 제공합니다. 또한 구성요소는 이 메서드는 제공자의 애플리케이션에 항상 전체 읽기 및 쓰기 액세스 권한을 가지 지정할 수 있습니다.
사용자 사전 제공자는
android.permission.READ_USER_DICTIONARY
권한이 있어야 합니다.
제공업체에는 별도의 android.permission.WRITE_USER_DICTIONARY
가 있습니다.
데이터 삽입, 업데이트 또는 삭제 권한이 있어야 합니다.
제공자에 액세스하는 데 필요한 권한을 얻으려면 애플리케이션은 자신의 매니페스트 파일에 있는 <uses-permission>
으로 권한을 요청합니다. Android 패키지 관리자가 애플리케이션을 설치할 때 사용자는
애플리케이션이 요청하는 모든 권한을 승인해야 합니다. 사용자가 승인하면
패키지 관리자가 설치를 계속합니다. 사용자가 승인하지 않으면 패키지 관리자가 거부함
설치를 중지합니다.
다음 샘플
<uses-permission>
요소가 사용자 사전 제공자에 읽기 권한을 요청합니다.
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
제공자 액세스에 대한 권한이 미치는 영향은 보안 도움말
데이터 삽입, 업데이트, 삭제
제공자로부터 데이터를 검색하는 것과 같은 방식으로 데이터를 수정할 때도 제공자 클라이언트와 제공자의 ContentProvider
간 상호작용을 사용합니다.
ContentProvider
의 해당하는 메서드에 전달된 인수로 ContentResolver
메서드를 호출합니다. 제공업체 및 제공업체
클라이언트가 보안 및 프로세스 간 통신을 자동으로 처리합니다.
데이터 삽입
데이터를 제공자에 삽입하려면 ContentResolver.insert()
메서드를 호출합니다. 이 메서드는 제공자에 새로운 행을 삽입하고 해당 열에 대한 콘텐츠 URI를 반환합니다.
다음 스니펫은 사용자 사전 제공자에 새 단어를 삽입하는 방법을 보여줍니다.
Kotlin
// Defines a new Uri object that receives the result of the insertion lateinit var newUri: Uri ... // Defines an object to contain the new values to insert val newValues = ContentValues().apply { /* * Sets the values of each column and inserts the word. The arguments to the "put" * method are "column name" and "value". */ 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, // The UserDictionary content URI newValues // The values to insert )
자바
// Defines a new Uri object that receives the result of the insertion Uri newUri; ... // Defines an object to contain the new values to insert ContentValues newValues = new ContentValues(); /* * Sets the values of each column and inserts the word. The arguments to the "put" * method are "column name" and "value". */ newValues.put(UserDictionary.Words.APP_ID, "example.user"); newValues.put(UserDictionary.Words.LOCALE, "en_US"); newValues.put(UserDictionary.Words.WORD, "insert"); newValues.put(UserDictionary.Words.FREQUENCY, "100"); newUri = getContentResolver().insert( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI newValues // The values to insert );
새로운 행의 데이터는 단일 행 커서와 형태가 유사한 단일 ContentValues
객체로 이동합니다. 이 객체 내의 열은 모두 같은 데이터 유형을 가지지 않아도 됩니다. 또한 값을 지정하고 싶지 않은 경우라면 ContentValues.putNull()
을 사용하여 열을 null
로 설정할 수 있습니다.
이전 스니펫은 _ID
열을 추가하지 않습니다. 이 열이 유지되기 때문입니다.
자동으로 확장 및 축소할 수 있습니다 제공자는 추가된 모든 열마다 고유한 _ID
값을 할당합니다. 제공자는 보통 이 값을 테이블의 기본 키로 사용합니다.
newUri
에 반환된 콘텐츠 URI는 새로 추가된 행을 식별합니다.
다음과 같은 형식을 사용합니다.
content://user_dictionary/words/<id_value>
<id_value>
는 새로운 행에 대한 _ID
의 콘텐츠입니다.
대부분의 제공자는 이런 형태의 콘텐츠 URI를 자동으로 감지할 수 있으며, 그런 다음 해당 행에서 요청된 작업을 실행합니다.
반환된 Uri
에서 _ID
값을 가져오려면 ContentUris.parseId()
를 호출합니다.
데이터 업데이트
행을 업데이트하려면 업데이트된ContentValues
값에 대해 쿼리와 마찬가지로 삽입 및 선택 기준을 적용할 수 있습니다.
사용하는 클라이언트 메서드는 ContentResolver.update()
입니다. 값은 업데이트하는 열의 ContentValues
객체에만 추가하면 됩니다. 열의 콘텐츠를 삭제하려면 값을 null
로 설정하세요.
다음 스니펫은 언어가 "en"
인 모든 행을
언어가 null
이어야 합니다. 반환 값은 업데이트된 행의 수입니다.
Kotlin
// Defines an object to contain the updated values val updateValues = ContentValues().apply { /* * Sets the updated value and updates the selected words. */ putNull(UserDictionary.Words.LOCALE) } // Defines selection criteria for the rows you want to update val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?" val selectionArgs: Array<String> = arrayOf("en_%") // Defines a variable to contain the number of updated rows var rowsUpdated: Int = 0 ... rowsUpdated = contentResolver.update( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI updateValues, // The columns to update selectionClause, // The column to select on selectionArgs // The value to compare to )
자바
// Defines an object to contain the updated values ContentValues updateValues = new ContentValues(); // Defines selection criteria for the rows you want to update String selectionClause = UserDictionary.Words.LOCALE + " LIKE ?"; String[] selectionArgs = {"en_%"}; // Defines a variable to contain the number of updated rows int rowsUpdated = 0; ... /* * Sets the updated value and updates the selected words. */ updateValues.putNull(UserDictionary.Words.LOCALE); rowsUpdated = getContentResolver().update( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI updateValues, // The columns to update selectionClause, // The column to select on selectionArgs // The value to compare to );
호출 시 사용자 입력 삭제
ContentResolver.update()
자세히 알아보려면
자세한 내용은 악의적인 입력으로부터 보호 섹션을 참고하세요.
데이터 삭제
행을 삭제하는 것은 행 데이터를 검색하는 것과 비슷합니다. 행의 선택 기준을 지정합니다.
클라이언트 메서드가 삭제된 행 수를 반환합니다.
다음 스니펫은 앱 ID가 "user"
와 일치하는 행을 삭제합니다. 메서드는 삭제된 행 수를 반환합니다.
Kotlin
// Defines selection criteria for the rows you want to delete val selectionClause = "${UserDictionary.Words.APP_ID} LIKE ?" val selectionArgs: Array<String> = arrayOf("user") // Defines a variable to contain the number of rows deleted var rowsDeleted: Int = 0 ... // Deletes the words that match the selection criteria rowsDeleted = contentResolver.delete( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI selectionClause, // The column to select on selectionArgs // The value to compare to )
자바
// Defines selection criteria for the rows you want to delete String selectionClause = UserDictionary.Words.APP_ID + " LIKE ?"; String[] selectionArgs = {"user"}; // Defines a variable to contain the number of rows deleted int rowsDeleted = 0; ... // Deletes the words that match the selection criteria rowsDeleted = getContentResolver().delete( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI selectionClause, // The column to select on selectionArgs // The value to compare to );
호출 시 사용자 입력 삭제
ContentResolver.delete()
자세히 알아보려면
악의적인 입력으로부터 보호 섹션을 참고하세요.
제공자 데이터 유형
콘텐츠 제공자는 아주 다양한 데이터 유형을 제공할 수 있습니다. 사용자 사전 제공자는 텍스트만 제공하지만, 제공자는 다음과 같은 형식도 제공할 수 있습니다.
- 정수
- 긴 정수(long)
- 부동 소수점
- 긴 부동 소수점(double)
제공자가 자주 사용하는 또 다른 데이터 유형은
64KB 바이트 배열. Cursor
클래스 'get' 메서드를 살펴보면 사용 가능한 데이터 유형을 확인할 수 있습니다.
제공자 내의 각 열에 대한 데이터 유형은 대개 제공자 문서에 나열되어 있습니다.
사용자 사전 제공자의 데이터 유형은 참조 문서에 나열되어 있습니다.
(계약 클래스 UserDictionary.Words
) 계약 클래스는
계약 클래스 섹션에 설명되어 있습니다.
Cursor.getType()
을 호출하여 데이터 유형을 확인할 수도 있습니다.
제공자는 스스로 정의하는 각 콘텐츠 URI의 MIME 데이터 유형 정보도 유지관리합니다. 다음과 같은 작업을 할 수 있습니다. MIME 유형 정보를 사용하여 애플리케이션이 MIME 유형을 기반으로 처리 유형을 선택할 수 있습니다. 일반적으로 복잡한 내용이 포함된 제공자와 작업할 때의 MIME 유형 데이터 구조 또는 파일일 수 있습니다.
예를 들어 연락처 제공자 내의 ContactsContract.Data
테이블은 MIME 유형을 사용하여 각 행에 저장된 연락처 데이터의 유형에 라벨을 지정합니다. 콘텐츠 URI에 상응하는 MIME 유형을 가져오려면 ContentResolver.getType()
을 호출하세요.
MIME 유형 참조 섹션에서는 문법을 지원합니다.
제공자 액세스의 대체 형식
애플리케이션 개발에 있어 중요한 제공자 액세스의 대체 형식에는 다음과 같은 세 가지가 있습니다.
-
일괄 액세스: 다음 위치에 있는 메서드로 일괄 액세스 호출을 만들 수 있습니다.
ContentProviderOperation
클래스에 적용하고ContentResolver.applyBatch()
입니다. -
비동기 쿼리: 별도의 스레드에서 쿼리를 실행합니다. 다음과 같은 작업을 할 수 있습니다.
CursorLoader
객체를 사용합니다. 사용 방법은 로더 가이드에 있는 예시에서 설명합니다. - 인텐트를 사용한 데이터 액세스: 인텐트를 보낼 수 없음 제공자에 직접 전달하려면 제공자의 애플리케이션에 인텐트를 보낼 수 있습니다. 일반적으로 제공자의 데이터를 수정하는 데 가장 적합합니다.
일괄 액세스와 인텐트를 사용한 수정은 다음 섹션에서 설명합니다.
일괄 액세스
제공자에 대한 일괄 액세스는 많은 수의 행을 삽입하거나 행을 생성하고 일반적으로 작업을 원자적 작업이라고 부르는 트랜잭션으로 프로세스 경계를 가로지르는 모든 작업을 처리합니다.
일괄 모드에서 제공업체에 액세스하려면
ContentProviderOperation
객체의 배열을 만든 다음
다음 명령어로 콘텐츠 제공자에게
ContentResolver.applyBatch()
입니다. 사용자는
이 메서드에 콘텐츠 제공자의 권한을 부여합니다.
이렇게 하면 배열의 각 ContentProviderOperation
객체가 작동할 수 있습니다.
비교할 수 있습니다. ContentResolver.applyBatch()
호출은 결과 배열을 반환합니다.
ContactsContract.RawContacts
계약 클래스의 설명에 일괄 삽입을 설명하는 코드 스니펫이 포함되어 있습니다.
인텐트를 사용한 데이터 액세스
인텐트는 콘텐츠 제공자에 간접 액세스를 제공할 수 있습니다. 사용자가 자신의 사이트에 애플리케이션에 액세스 권한이 없더라도 권한이 있는 애플리케이션에서 결과 인텐트를 다시 가져오거나 그 안에서 작업할 수 있도록 허용하는 애플리케이션입니다.
임시 권한으로 액세스
적절한 액세스 권한이 없더라도 콘텐츠 제공업체의 데이터에 액세스할 수 있습니다. 권한을 보유하고, 이 권한을 보유하고 있는 애플리케이션에 인텐트를 보내서 URI 권한이 포함된 결과 인텐트를 다시 수신합니다. 이들 권한은 특정 콘텐츠 URI에 대한 권한으로, 이를 수신하는 활동이 완료될 때까지 유지됩니다. 영구 권한을 가지고 있는 애플리케이션은 결과 인텐트에 다음과 같이 플래그를 설정하여 임시 권한을 허가합니다.
참고: 이와 같은 플래그는 콘텐츠 URI에 권한이 들어 있는 제공자에 일반적인 읽기 또는 쓰기 액세스 권한을 부여하지는 않습니다. 이 액세스는 URI 자체에만 해당됩니다.
콘텐츠 URI를 다른 앱에 전송할 때 다음 중 하나 이상을 포함하세요. 있습니다. 플래그는 Android 11 (API 수준 30) 이상을 타겟팅하는 인텐트:
- 콘텐츠 URI가 나타내는 데이터에서 읽거나 씁니다. 지정될 수 있습니다.
- 패키지 확보 일치하는 콘텐츠 제공자가 포함된 앱에 대한 공개 상태가 URI 권한입니다. 인텐트를 전송하는 앱과 두 개의 다른 앱일 수 있습니다.
제공자는 자신의 매니페스트에 있는 콘텐츠 URI에 대한 URI 권한을 정의하고, 이때
android:grantUriPermissions
속성
<provider>
요소뿐 아니라
<ph type="x-smartling-placeholder"><grant-uri-permission>
</ph>
이
<provider>
요소가 포함됩니다. URI 권한 메커니즘은
Android에서의 권한 가이드를 참조하세요.
예를 들어 READ_CONTACTS
권한이 없더라도 연락처 제공자 내의 연락처에 대한 데이터를 검색할 수 있습니다. 이 작업을 하면 좋은 예로, 연락처에 기재된 사람의 생일에 전자 축하 카드를 보내주는 애플리케이션을 들 수 있습니다. 대신
READ_CONTACTS
을(를) 요청하여
직접 관리할 수 있도록
연락처 정보를 포함할 수 있습니다. 이렇게 하려면 다음 절차를 따르세요.
-
애플리케이션에서 작업이 포함된 인텐트를 전송합니다.
ACTION_PICK
및 '연락처' MIME 유형CONTENT_ITEM_TYPE
: 메서드startActivityForResult()
). - 이 인텐트는 피플 앱의 '선택' 해당 활동이 포그라운드로 나옵니다.
-
선택 활동에서 사용자가 업데이트할 연락처를 선택합니다. 이렇게 되면 선택 활동이
setResult(resultcode, intent)
를 호출하여 애플리케이션에 돌려줄 인텐트를 설정합니다. 콘텐츠 URI가 포함된 인텐트 사용자가 선택한 연락처와 '추가' 플래그FLAG_GRANT_READ_URI_PERMISSION
이들 플래그는 앱에 URI 권한을 부여하여 콘텐츠 URI가 가리키는 연락처의 데이터를 읽을 수 있도록 합니다. 그런 다음 선택 활동은finish()
를 호출하여 애플리케이션에 제어를 반환합니다. -
활동이 포그라운드로 돌아오고 시스템이 활동의
onActivityResult()
메서드를 호출합니다. 이 메서드가 피플 앱의 선택 활동이 생성한 결과 인텐트를 수신합니다. - 결과 인텐트로부터 받은 콘텐츠 URI를 사용하면 연락처 제공자에서 연락처의 데이터를 읽을 수 있습니다. 이것은 매니페스트에서 제공자로의 영구 읽기 권한을 요청하지 않았어도 적용됩니다. 그러면 연락처의 생일 정보를 받을 수 있습니다. 또는 이메일 주소로 보낸 다음 전자 인사말을 보냅니다.
다른 애플리케이션 사용
액세스 권한이 없는 데이터를 사용자가 수정하도록 하는 또 다른 방법은 활성화하고 사용자가 거기서 작업을 수행하도록 할 수 있습니다.
예를 들어 캘린더 애플리케이션은
ACTION_INSERT
인텐트에 대한
삽입 UI에 있습니다. 이 인텐트에 'extras' 데이터를 전달할 수 있습니다. 이 데이터는 애플리케이션이 UI를 미리 채우는 데 사용됩니다. 반복적인 이벤트의 구문은 복잡하므로 캘린더 제공자에 이벤트를 삽입하려면 ACTION_INSERT
로 캘린더 앱을 활성화하고 사용자에게 그곳에서 이벤트를 삽입하게 하는 것이 좋습니다.
도우미 앱을 사용하여 데이터 표시
애플리케이션에 액세스 권한이 있는 경우에도
인텐트를 사용하여 다른 애플리케이션에 데이터를 표시할 수 있습니다. 예를 들어 캘린더 애플리케이션은
특정 날짜 또는 이벤트를 표시하는 ACTION_VIEW
인텐트입니다.
이렇게 하면 UI를 직접 만들지 않고도 캘린더 정보를 표시할 수 있습니다.
이 기능에 대한 자세한 내용은
캘린더 제공자 개요
인텐트를 보낼 애플리케이션이 아니어도 됩니다.
제공업체로 연결됩니다. 예를 들어 연락처 제공자에서 연락처를 검색한 다음, 해당 연락처의 이미지에 대한 콘텐츠 URI가 들어 있는 ACTION_VIEW
인텐트를 이미지 뷰어로 보낼 수 있습니다.
계약 클래스
계약 클래스는 애플리케이션이 콘텐츠 URI, 열 이름, 인텐트 작업 및 콘텐츠 제공자의 다른 기능과 작업할 수 있게 도와주는 상수를 정의합니다. 계약 클래스는
제공업체에 자동으로 포함됩니다 제공업체의 개발자가 이를 정의하고
다른 개발자에게 제공할 수 있게 됩니다. Android 플랫폼 내에 포함된 제공자는 대부분 패키지 android.provider
안에 상응하는 계약 클래스를 가지고 있습니다.
예를 들어 사용자 사전 제공자에는 콘텐츠 URI와 열 이름 상수가 들어 있는 UserDictionary
계약 클래스가 있습니다. 이
Words
테이블의 콘텐츠 URI가 상수에 정의됩니다.
UserDictionary.Words.CONTENT_URI
입니다.
UserDictionary.Words
클래스에는 열 이름 상수도 포함되어 있습니다.
이는 이 가이드의 예시 스니펫에 사용된 것입니다. 예를 들어 쿼리 프로젝션은
다음과 같이 정의됩니다.
Kotlin
val projection : Array<String> = arrayOf( UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.LOCALE )
자바
String[] projection = { 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 유형이라고도 함)에는 복잡한 type 및 subtype 값입니다. 여러 행의 유형 값은 항상 다음과 같습니다.
vnd.android.cursor.dir
단일 행의 유형 값은 항상 다음과 같습니다.
vnd.android.cursor.item
subtype는 제공자별로 다릅니다. Android 내장 제공자는 보통 단순한 하위유형을 가지고 있습니다. 예를 들어 주소록 애플리케이션이 전화번호 행을 생성하는 경우 애플리케이션은 해당 행에 다음과 같은 MIME 유형을 설정합니다.
vnd.android.cursor.item/phone_v2
하위유형 값은 phone_v2
입니다.
다른 제공자 개발자는 제공자의
권한 및 테이블 이름이 있습니다 예를 들어 기차 시간표가 들어 있는 제공자가 있다고 생각해 보세요.
제공자의 권한은 com.example.trains
이고 그 안에 테이블 Line1, Line2, Line3이 들어 있습니다. 테이블 Line1에 대한 다음 콘텐츠 URI에 대한 응답입니다.
content://com.example.trains/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
는 단일 연락처 행의 MIME 유행에 관한 상수 CONTENT_ITEM_TYPE
을 정의합니다.
단일 행에 대한 콘텐츠 URI는 콘텐츠 URI 섹션을 참조하세요.