본문 바로가기
안드로이드

내용 제공자

by 희안 2021. 5. 4.

내용 제공자는 콘텐트 프로바이더라고 부르며 한 앱에서 관리하는 데이터를 다른 앱에서도 접근할 수 있도록 해준다. 내용제공자도 앱 구성요소여서 시스템에서 관리한다. 즉, 메니페스트에 등록해주어야 한다. 내용제공자는 앱의 보안에 중요한 역할을 하는데 각 앱이 자신의 프로세스와 권한 안에서만 데이터에 접근할 수 있기 때문이다. 내용제공자는 이때 다른 앱에 접근할 수 있도록 데이터 접근 통로를 열어줄 수 있고 이 허용된 통로로만 접근할 수 있다. 

 

내용제공자에서 공유할 수 있는 데이터 : 데이터베이스, 파일, SharedPreferences

 

이 중 데이터베이스에 접근하는게 가장 일반적이다. 내용제공자는 CRUD에 대응하는 insert(), query(), update(), delete()메서드를 지원하고 내용제공자에서 허용한 통로로 접근하려면 콘텐트 리졸버 객체가 필요하다. 예제로 내용제공자를 포함하는 앱을 만들고, 콘텐트 리졸버를 포함하는 앱을 만들어보자.

public class PersonProvider extends ContentProvider {
    private static final String AUTHORITY = "android.develop.hello";
    private static final String BASE_PATH = "person";
    public static final Uri CONTENT_URI = Uri.parse("content://"+AUTHORITY+"/"+BASE_PATH);

    private static final int PERSONS = 1;
    private static final int PERSON_ID = 2;

    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static{
        uriMatcher.addURI(AUTHORITY, BASE_PATH, PERSONS);
        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 PERSONS:
                cursor = database.query(DatabaseHelper.TABLE_NAME,
                        DatabaseHelper.ALL_COLUMNS,
                        selection, null, null, null, DatabaseHelper.PERSON_NAME+" ASC");
                break;
            default:
                throw new IllegalArgumentException("not knowk URI "+uri);
        }

        return cursor;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        switch (uriMatcher.match(uri)) {
            case PERSONS:
                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 PERSONS:
                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 PERSONS:
                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 테이블 이름을 합쳐서 정의했다. content URI를 정의하는 형식은 content://org.techtown.provider/person/1 이런식으로 content, authority(특정 내용제공자 구분하는 고유값), base path(요청할 데이터의 자료형 결정함. 여기선 DB), id(요청할 데이터 레코드를 지정)로 지정할 수 있다. 

 

UriMatcher 객체는 URI를 매칭하는것에 사용된다. match()를 호출하면 UriMatcher에 addURI()를 사용해 추가된 URI중 실행가능한 것이 있는지 확인해준다. 그 후 이 안에서 내용제공자에 접근하기 위해 ContentResolver 객체가 사용된다. 액티비티에서 getContentResolver()를 호출한 뒤 query(), insert() 등을 사용할 수 있다. notifyChange()는 레코드가 변경되었을때 알려주는 역할을 한다. 내용제공자를 이용해 값을 조회하기 위해선 query()를 사용한다. 

 

Cursor query ( Uri, String[], String, String[], String)

1: URI, 2: 어떤 칼럼 조회할지, 3: where 절 조건, 4: 3번째에 값이 있으면 그 안에 들어갈 건 값을 대체, 5: 정렬칼럼을 지정.

이런식으로 insert, update, delete를 모두 지정해서 활용할 수 있다. getType()은 MIME 타입이 무엇인지 알고싶을 때 사용할 수 있다. MIME타입을 알 수 없는 경우 NULL이 반환된다. 

 

Insert

insert안에선 Uri 객체를 만들고 ContentResolver의 query()를 호출하면서 Uri 객체를 파라미터로 전달한다. 문자열에서 Uri 객체를 만들땐 new 연산자를 이용해 Builder 객체를 만든 후 build()와 parse()를 호출하면서 문자열을 파라미터로 전달한다. ContentResolver는 getContentResolver()로 참조하고 query()를 호출해서 결과로 Cursor 객체를 받는다. 레코드를 추가할 땐 ContentValues객체가 사용되는데 칼럼이름을 getColumnNames()로 받아서 지정하거나 임의로 지정할 수 있다. ContentResolver의 insert를 호출해서 레코드를 추가할땐 URI 객체와함께 ContentValues객체를 파라미터로 지정한다. 

 

query

Cursor 객체를 반환받은 뒤 query()를 호출할 때 URI이외에 columns도 전달하는데 여기엔 칼럼들의 이름이 배열형태로 들어가 있다. Cursor 객체는 moveToNext()로 결과 레코드를 넘길 수 있으므로 while문 안에서 각 레코드 값을 출력한다.

 

update 

selection안의 mobile = ? 에서는 안의 ? 기호에는 selectionArgs 배열변수의 첫번째 원소로 대체된다. 

 

delete

delete에서도 ? 를 대체할 값으로 selectionArgs안의 변수가 들어간다. 

public void insertPerson(){
        println("insertPerson call");
        String string = "content://android.develop.hello/person";
        Uri uri = new Uri.Builder().build().parse(string);

        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());
    }

    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 age = cursor.getInt(cursor.getColumnIndex(columns[1]));
                String mobile = cursor.getString(cursor.getColumnIndex(columns[2]));

                println("#" + index + " -> " + name + ", " + age + ", " + mobile);
                index += 1;
            }

        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    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);
    }

    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);
    }

 

메니페스트

메니페스트 안에 permission들을 넣어주어야 한다. READ_DATABASE, WRITE_DATABASE

<provider
            android:authorities="android.develop.hello"
            android:name=".PersonProvider"
            android:exported="true"
            android:readPermission="android.develop.hello.READ_DATABASE"
            android:writePermission="android.develop.hello.WRITE_DATABASE"/>

중간에 Create 문에서 잘못써서 데이터베이스가 이상하게 만들어지는 오류가 발생했었다. 그 후로는 코드를 알맞게 고쳐도 이상하게 이미 반영이 되었기 때문에 데이터베이스 버전을 변경해주거나 어플을 re install 해주어야 해결된다.

 

댓글