내용 제공자는 한 앱에서 관리하는 데이터를 다른 앱에서도 접근할 수 있도록 해준다.
내용 제공자가 필요한 이유는 앱의 보안때문이다.
보통 앱은 자신의 프로세스와 권한 안에서만 데이터에 접근 할 수 있도록 되어있다. 하지만 가끔은 서로 다른 앱의 데이터에 접근해야 하는 경우도 있는데, 그때 내용 제공자를 사용하면 된다.
내용 제공자에서 공유할 수 있는 데이터는
· 데이터베이스
· 파일
· SharedPreferences
가 있는데, 이 중 데이터베이스에 접근하는 것이 가장 일반적이다.
내용 제공자도 앱의 구성요소이기 때문에 매니페스트 파일에 등록해야 사용할 수 있다.
내용 제공자를 이용해 다른 앱의 파일에 접근할 때는 반드시 허용된 통로로만 접근해야 하며,
이 통로에 접근하려면 콘텐트 리졸버(ContentResolver) 객체가 필요하다.
activity_main.xml 파일에 화면 레아이웃을 만들었다.
화면에는 insert, query, update, delete 네 개의 버튼과
ScrollView 안에 TextView 하나를 배치하였다.
그 후 MainActivity.java에 버튼을 클릭했을 때 이벤트 처리를 수행할 수 있는 기본 코드를 입력하였다.
package org.techtown.provider;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
Button buttonInsert = findViewById(R.id.buttonInsert);
buttonInsert.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
Button buttonQuery = findViewById(R.id.buttonQuery);
buttonQuery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
Button buttonUpdate = findViewById(R.id.buttonUpdate);
buttonUpdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
Button buttonDelete = findViewById(R.id.buttonDelete);
buttonDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
public void println(String data){
textView.append(data + '\n');
}
}
이 기능들은 뒤에서 추가할 것이다.
DatabaseHelper 클래스를 만들었다.
package org.techtown.provider;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "person.db";
private static final int DATABASE_VERSION = 1;
public static final String TABLE_NAME = "person";
public static final String PERSON_ID = "_id";
public static final String PERSON_NAME = "name";
public static final String PERSON_AGE = "age";
public static final String PERSON_MOBILE = "mobile";
public static final String[] ALL_COLUMNS = {PERSON_ID, PERSON_NAME, PERSON_AGE, PERSON_MOBILE};
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
PERSON_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
PERSON_NAME + " TEXT, " +
PERSON_AGE + " INTEGER, " +
PERSON_MOBILE + " TEXT" +
")";
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
DatabaseHelper 클래스는 SQLiteOpenHelper 클래스를 상속받고 있으며,
person.db 데이터베이스를 사용하며, 그 안에 person 테이블을 만들도록 하였다.
내용 제공자 클래스인 PersonProvider 클래스를 만들었다.
package org.techtown.provider;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class PersonProvider extends ContentProvider {
private static final String AUTHORITY = "org.techtown.provider";
private static final String BASE_PATH = "person";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
private static final int PERSON = 1;
private static final int PERSON_ID = 2;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, BASE_PATH, PERSON);
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", PERSON_ID);
}
private SQLiteDatabase database;
@Override
public boolean onCreate() {
DatabaseHelper helper = new DatabaseHelper(getContext());
database = helper.getWritableDatabase();
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Cursor cursor;
switch (uriMatcher.match(uri)){
case PERSON:
cursor = database.query(DatabaseHelper.TABLE_NAME,
DatabaseHelper.ALL_COLUMNS, selection,
null, null, null, DatabaseHelper.PERSON_NAME + " ASC");
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
return cursor;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)){
case PERSON:
return "vnd.android.cursor.dir/persons";
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
long id = database.insert(DatabaseHelper.TABLE_NAME, null, values);
if(id > 0){
Uri _uri = ContentUris.withAppendedId(CONTENT_URI, id);
getContext().getContentResolver().notifyChange(_uri, null);
return _uri;
}
throw new SQLException("추가 실패-> URI : " + uri);
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
int count = 0;
switch (uriMatcher.match(uri)){
case PERSON:
count = database.delete(DatabaseHelper.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
int count = 0;
switch (uriMatcher.match(uri)){
case PERSON:
count = database.update(DatabaseHelper.TABLE_NAME, values, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
}
내용 제공자를 만들기 위해서는 고유한 값을 가진 content URI를 만들어야 한다. 여기서는 앱의 패키지 이름과 person 테이블의 이름을 합쳐 URI를 정의하였다.
private static final String AUTHORITY = "org.techtown.provider";
private static final String BASE_PATH = "person";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
content URI를 정의하는 형식은 다음과 같다.
content://org.techtown.provider/person/1
content:// -> 내용 제공자에 의해 제어되는 데이터라는 의미로 항상 content://로 시작함
Authority -> org.techtown.provider 부분을 가리키며 특정 내용 제공자를 구분하는 고유한 값
Base Path -> person 부분을 가리키며 요청할 데이터의 자료형을 결정함(여기서는 테이블 이름)
ID -> 맨 뒤의 1과 같은 숫자를 가리키며 요청할 데이터 레코드를 지정함
UriMatcher 객체는 URI를 매칭하는데 사용된다.
private static final int PERSON = 1;
private static final int PERSON_ID = 2;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, BASE_PATH, PERSON);
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", PERSON_ID);
}
match 메소드를 호출하면 UriMatcher에 addURI 메소드를 이용해 추가된 URI 중에서 실행 가능한 것이 있는지 확인해준다.
switch (uriMatcher.match(uri)){
case PERSON:
...
PersonProvider 클래스에는 insert, query, update, delete 메소드가 정의되있다.
그리고 이 안에서는 내용 제공자에 접근하기 위하여 ContentResolver 객체도 사용한다.
액티비티에서 getContentResolver 메소드를 호출하면 ContentResolver 객체를 반환한다.
이 객체에는 query, insert, update, delete 등의 메소드가 정의되어 있어 내용 제공자의 URI를 파라미터로 전달하면서 데이터를 조회, 추가, 수정, 삭제하는 일이 가능하다.
getContext().getContentResolver().notifyChange(_uri, null);
notifyChange 메소드는 레코드가 추가, 수정, 삭제되었을 때 변경이 일어났음을 알려주는 역할을 한다.
내용 제공자를 이용해 값을 조회하고 싶다면 다음과 같은 query 메소드를 사용한다.
Cursor query (
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder
)
첫 번째 파라미터는 URI 값이고,
두 번째 파라미터는 어떤 칼럼들을 조회할 것인지를 지정한다. 만약 null 값을 전달하면 모든 칼럼을 조회한다.
세 번째 파라미터는 SQL에서 where 절에 들어갈 조건을 지정한다. 만약 null 값을 지정하면 where 절이 없다고 생각할 수 있다.
네 번째 파라미터는 세 번째 파라미터 값이 있을 경우 그 안에 들어갈 조건 값을 대체하기 위해 사용된다.
다섯번째 파리미터는 정렬 칼럼을 지정하며 null 값이면 정렬이 적용되지 않는다.
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Cursor cursor;
switch (uriMatcher.match(uri)){
case PERSON:
cursor = database.query(DatabaseHelper.TABLE_NAME,
DatabaseHelper.ALL_COLUMNS, selection,
null, null, null, DatabaseHelper.PERSON_NAME + " ASC");
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
return cursor;
}
내용 제공자를 이용해 값을 추가하고 싶다면 다음과 같은 insert 메소드를 사용한다.
Uri insert (
Uri uri
ContentValues values
)
첫 번째 파라미터는 URI이고,
두 번째 파라미터는 저장할 칼럼명과 값들이 들어간 ContentValues 객체다.
결과 값으로는 새로 추가된 값의 Uri 정보가 반환된다.
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
long id = database.insert(DatabaseHelper.TABLE_NAME, null, values);
if(id > 0){
Uri _uri = ContentUris.withAppendedId(CONTENT_URI, id);
getContext().getContentResolver().notifyChange(_uri, null);
return uri;
}
throw new SQLException("추가 실패-> URI : " + uri);
}
내용 제공자를 통해 값을 수정하고 싶다면 다음과 같은 update 메소드를 사용한다.
int update (
Uri uri
ContentValues values,
String[] selection,
String[] selectionArgs
)
첫 번째 파라미터는 URI이고,
두 번째 파라미터는 저장할 칼럼명과 값들이 들어간 ContentValues 객체이다. 두 번째 파라미터 값에 null이 들어가면 안된다.
세 번째 파리미터는 SQL에서 where 절에 들어갈 조건을 지정한다. null을 지정하면 where 절이 없다고 생각할 수 있다.
네 번째 파라미터는 세 번째 파리미터 값이 있을 경우 그 안에 들어갈 조건 값을 대체하기 위해 사용된다.
결과 값으로는 영향을 받은 레코드의 개수가 반환된다.
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
int count = 0;
switch (uriMatcher.match(uri)){
case PERSON:
count = database.update(DatabaseHelper.TABLE_NAME, values, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
내용 제공자를 이용해 값을 삭제하고 싶다면 다음과 같은 delete 메소드를 사용한다.
int delete (
Uri uri,
String selection,
String[] selectionArgs
)
첫 번째 파라미터는 URI이고,
두 번째 파리미터는 SQL에서 where 절에 들어갈 조건을 지정한다. null을 지정하면 where 절이 없다고 생각할 수 있다.
세 번째 파라미터는 세 번째 파리미터 값이 있을 경우 그 안에 들어갈 조건 값을 대체하기 위해 사용된다.
결과 값으로는 영향 받은 레코드의 개수가 반환된다.
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
int count = 0;
switch (uriMatcher.match(uri)){
case PERSON:
count = database.delete(DatabaseHelper.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
getType 메소드는 MIME 타입이 무엇인지 알고 싶을 때 사용한다.
String getType (
Uri uri
)
Uri 객체가 파리미터로 전달되며 결과 값으로 MIME 타입이 반환된다. 만약 MIME 타입을 알 수 없다면 null값이 반환된다.
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)){
case PERSON:
return "vnd.android.cursor.dir/persons";
default:
throw new IllegalArgumentException("알 수 없는 URI " + uri);
}
}
이제 MainActivity.java에 추가한 각각의 버튼을 눌렀을 때 insert, query, update, delete 메소드를 호출하도록 하고 그 결과를 화면에 표시하도록 코드를 추가해보겠다.
Button buttonInsert = findViewById(R.id.buttonInsert);
buttonInsert.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
insertPerson();
}
});
...
public void insertPerson() {
println("insertPerson 호출됨.");
String uriString = "content://org.techtown.provider/person";
Uri uri = new Uri.Builder().build().parse(uriString);
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
String[] columns = cursor.getColumnNames();
println("columns count -> " + columns.length);
for(int i = 0; i < columns.length; i++){
println("#" + i + " : " + columns[i]);
}
ContentValues values = new ContentValues();
values.put("name", "John");
values.put("age", 20);
values.put("mobile", "010-1000-1000");
uri = getContentResolver().insert(uri, values);
println("insert 결과 -> " + uri.toString());
}
첫 번째 버튼을 누르면 insertPerson 메소드가 호출된다.
buttonInsert.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
insertPerson();
}
});
insertPerson 메소드 안에서는 먼저 Uri 객체를 만들고 ContentResolver 객체의 query 메소드를 호출하면서 Uri 객체를 파라미터로 전달한다.
문자열에서 Uri 객체를 만들 때는 new 연산자를 이용해 Uri.Builder 객체를 먼저 만든 후 build와 parse 메소드를 호출하면서 문자열을 파라미터로 전달한다.
ContentResolver 객체는 getContentResolver 메소드를 호출하면 참조할 수 있으며 query 메소드를 호출하면 그 결과 값으로 Cursor 객체가 반환된다.
String uriString = "content://org.techtown.provider/person";
Uri uri = new Uri.Builder().build().parse(uriString);
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
Cursor 객체를 이용해 결과 값을 조회할 수 있는데 결과 레코드에 들어가 있는 칼럼의 이름을 조회하고 싶다면 getColumnNames 메소드를 사용할 수 있다.
getColumnNames 메소드 이용해 알아낸 칼럼 이름을 화면에 출력하여 확인하였다.
String[] columns = cursor.getColumnNames();
println("columns count -> " + columns.length);
for(int i = 0; i < columns.length; i++){
println("#" + i + " : " + columns[i]);
}
레코드를 추가할 때는 ContentValues 객체가 사용되는데 getColumnNames 메소드를 이용해 알아낸 칼럼 이름을 사용할 수도 있고 직접 칼럼 이름을 지정할 수도 있다.
여기에서는 칼럼 이름을 직접 지정하였다.
ContentValues values = new ContentValues();
values.put("name", "John");
values.put("age", 20);
values.put("mobile", "010-1000-1000");
ContentResolver의 insert 메소드를 호출하여 레코드를 추가할 때는 Uri 객체와 함께 ContentValues 객체를 파라미터로 전달한다.
uri = getContentResolver().insert(uri, values);
println("insert 결과 -> " + uri.toString());
Button buttonQuery = findViewById(R.id.buttonQuery);
buttonQuery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
queryPerson();
}
});
...
public void queryPerson() {
try{
String uriString = "content://org.techtown.provider/person";
Uri uri = new Uri.Builder().build().parse(uriString);
String[] columns = new String[] {"name", "age", "mobile"};
Cursor cursor = getContentResolver().query(uri, columns, null, null, "name ASC");
println("query 결과 : " + cursor.getCount());
int index = 0;
while (cursor.moveToNext()){
//String name = cursor.getString(cursor.getColumnIndex(columns[0]));
int name_index = cursor.getColumnIndex(columns[0]);
String name = cursor.getString(name_index);
//int age = cursor.getInt(cursor.getColumnIndex(columns[1]));
int age_index = cursor.getColumnIndex(columns[1]);
int age = cursor.getInt(age_index);
//String mobile = cursor.getString(cursor.getColumnIndex(columns[2]));
int mobile_index = cursor.getColumnIndex(columns[2]);
String mobile = cursor.getString(mobile_index);
println("#" + index + " -> " + name + ", " + age + ", " + mobile);
index += 1;
}
}catch (Exception e){
e.printStackTrace();
}
}
두 번째 버튼을 누르면 queryPerson 메소드가 호출된다.
Button buttonQuery = findViewById(R.id.buttonQuery);
buttonQuery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
queryPerson();
}
});
첫 번째 버튼을 눌러 레코드를 추가할 때와 유사하다.
먼저 Uri 객체를 만들고 CotentResolver 객체의 query 메소드를 호출하여 Uri 객체를 전달하여 Cursor 객체를 반환한다.
String uriString = "content://org.techtown.provider/person";
Uri uri = new Uri.Builder().build().parse(uriString);
String[] columns = new String[] {"name", "age", "mobile"};
Cursor cursor = getContentResolver().query(uri, columns, null, null, "name ASC");
println("query 결과 : " + cursor.getCount());
query 메소드를 호출할 때 Uri 객체 외에도 columns도 전달하고 있는데 여기엔 조회할 칼럼의 이름이 들어가 있다. 이렇게 하면 지정한 칼럼만 조회할 수 있다.
Cursor 객체가 반환되면 각 칼럼 이름에 해당하는 칼럼 인덱스 값을 확인한 후 칼럼 값을 조회한다.
int index = 0;
while (cursor.moveToNext()){
//String name = cursor.getString(cursor.getColumnIndex(columns[0]));
int name_index = cursor.getColumnIndex(columns[0]);
String name = cursor.getString(name_index);
//int age = cursor.getInt(cursor.getColumnIndex(columns[1]));
int age_index = cursor.getColumnIndex(columns[1]);
int age = cursor.getInt(age_index);
//String mobile = cursor.getString(cursor.getColumnIndex(columns[2]));
int mobile_index = cursor.getColumnIndex(columns[2]);
String mobile = cursor.getString(mobile_index);
println("#" + index + " -> " + name + ", " + age + ", " + mobile);
index += 1;
}
Button buttonUpdate = findViewById(R.id.buttonUpdate);
buttonUpdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
updatePerson();
}
});
...
public void updatePerson() {
String uriString = "content://org.techtown.provider/person";
Uri uri = new Uri.Builder().build().parse(uriString);
String selection = "mobile = ?";
String[] selectionArgs = new String[] {"010-1000-1000"};
ContentValues updateValue = new ContentValues();
updateValue.put("mobile", "010-2000-2000");
int count = getContentResolver().update(uri, updateValue, selection, selectionArgs);
println("update 결과 : " + count);
}
세 번째 버튼을 눌렀을 때는 updatePerson 메소드를 호출한다.
Button buttonUpdate = findViewById(R.id.buttonUpdate);
buttonUpdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
updatePerson();
}
});
updatePerson 메소드 안에서는 mobile 칼럼의 값이 010-1000-1000인 레코드만 010-2000-2000으로 수정하도록 한다.
String uriString = "content://org.techtown.provider/person";
Uri uri = new Uri.Builder().build().parse(uriString);
String selection = "mobile = ?";
String[] selectionArgs = new String[] {"010-1000-1000"};
ContentValues updateValue = new ContentValues();
updateValue.put("mobile", "010-2000-2000");
int count = getContentResolver().update(uri, updateValue, selection, selectionArgs);
println("update 결과 : " + count);
수정할 때는 ContentResolver의 update 메소드를 호출하면서 Uri 객체, ContentValues 객체, where 조건, where 조건의 ? 기호를 대체할 값을 차례로 넣어준다.
where 조건의 문자열에 'mobile = ?' 를 넣었으므로 ? 기호가 하나 들어있다. 이 기호는 selectonArgs 배열 변수의 첫 번째 원소로 대체된다. 따라서 where 조건은 'mobile = 010-2000-2000'이 된다.
Button buttonDelete = findViewById(R.id.buttonDelete);
buttonDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deletePerson();
}
});
}
...
public void deletePerson() {
String uriString = "content://org.techtown.provider/person";
Uri uri = new Uri.Builder().build().parse(uriString);
String selection= "name = ?";
String[] selectionArgs = new String[] {"John"};
int count = getContentResolver().delete(uri, selection, selectionArgs);
println("delete 결과 : " + count);
}
네 번째 버튼을 눌렀을 때는 deletePerson 메소드를 호출한다.
Button buttonDelete = findViewById(R.id.buttonDelete);
buttonDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deletePerson();
}
});
deletePerson 메소드 안에는 ContentResolver의 delete 메소드를 호출하면서 Uri 객체, where 조건, where 조건의 ? 기호를 대체할 값을 차례로 넣어준다.
public void deletePerson() {
String uriString = "content://org.techtown.provider/person";
Uri uri = new Uri.Builder().build().parse(uriString);
String selection= "name = ?";
String[] selectionArgs = new String[] {"John"};
int count = getContentResolver().delete(uri, selection, selectionArgs);
println("delete 결과 : " + count);
}
마지막으로 메니페스트에 내용 제공자를 등록한다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.techtown.provider">
<permission android:name="org.techtown.provider.READ_DATABASE" android:protectionLevel="normal"/>
<permission android:name="org.techtown.provider.WRITE_DATABASE" android:protectionLevel="normal"/>
<application
...
<provider
android:authorities="org.techtown.provider"
android:name=".PersonProvider"
android:exported="true"
android:readPermission="org.techtown.provider.READ_DATABASE"
android:writePermission="org.techtown.provider.WRITE_DATABASE"
/>
</application>
</manifest>
permission 태그로 두 개의 권한을 새로 정의하였다.
<permission android:name="org.techtown.provider.READ_DATABASE" android:protectionLevel="normal"/>
<permission android:name="org.techtown.provider.WRITE_DATABASE" android:protectionLevel="normal"/>
name 속성 값으로는 "org.techtown.provider.READ_DATABASE"와 "org.techtown.provider.WRITE_DATABASE"가 설정되었으며, protectionLevel 속성 값으로는 "normal"이 설정되었다.
application 태그 안에는 provider 태그를 추가하고 authorities와 name, readPermission, writePermission 등의 속성을 추가하였다.
<provider
android:authorities="org.techtown.provider"
android:name=".PersonProvider"
android:exported="true"
android:readPermission="org.techtown.provider.READ_DATABASE"
android:writePermission="org.techtown.provider.WRITE_DATABASE"
/>
authorities 속성은 내용 제공자를 정의할 때 설정한 authorities 값과 동일하게 넣어준다.
name 속성으로는 내용 제공자 클래스인 PersonProvider 클래스를 설정한다.
readPermission 속성으로는 위에서 정의한 "org.techtown.provider.READ_DATABASE" 권한을 지정하고
writePermisson 속성으로는 위에서 정의한 "org.techtown.provider.WRITE_DATABASE" 권한을 지정한다.
여기서는 내용 제공자를 정의한 앱에서 데이터를 추가하고 조회했지만, 다른 앱에서도 내용 제공자를 이용하면 이 앱에서 관리하는 데이터를 조회할 수 있게 된다.