티스토리 뷰

내용 제공자(Content provider)는 한 앱에서 관리하는 데이터를 다른 앱에서도 접근할 수 있도록 해주는 안드로이드 어플의 구성 요소이다. 

 

내용 제공자를 사용하면 다른 앱이 열어둔 데이터 접근 통로를 이용할 수 있으며, 반대로 다른 앱에게 데이터 접근 통로를 열어줄 수 있다.

 

내용 제공자에서 공유할 수 있는 데이터는 다음과 같다.

  • 데이터베이스
  • 파일
  • SharedPreference

 

데이터베이스를 다룰 때는 CRUD 동작을 모두 사용할 수 있다.

 

기본적으로 내용 제공자에서 허용한 통로로 접근을 하려면, ContentResolver객체가 필요하다.

 

ContentResolver 객체는 데이터 제공자와 통신하여 ContentProvider를 구현하는 객체의 인스턴스를 가져오게 되고, 해당 provider 객체를 통해서 데이터를 요청하고 결과를 수신하게 된다.

 

예제 코드와 함께 확인해보자.


우선 내용 제공자 또한 앱의 구성요소 중 하나이기 때문에 매니페스트 파일에 선언해줘야 한다. 또한 앱의 데이터베이스를 열람하고, 수정할 예정이기 때문에 권한 설정도 진행해준다.

 

◎AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.samplecontentprovider">

    <permission android:name="com.example.samplecontentprovider.READ_DATABASE" android:protectionLevel="normal" />
    <permission android:name="com.example.samplecontentprovider.WRITE_DATABASE" android:protectionLevel="normal" />

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.SampleContentProvider">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <provider
            android:authorities="com.example.samplecontentprovider" android:name=".PersonProvider"
            android:exported="true"
            android:readPermission="com.example.samplecontentprovider.READ_DATABASE"
            android:writePermission="com.example.samplecontentprovider.WRITE_DATABASE"
        />
    </application>

</manifest>

 

이후 이 앱에서 사용할 데이터베이스를 생성하고, 스키마 관리를 하기 위해 직전 포스팅에서 다뤘던 SQLiteOpenHelper객체를 생성한다. 

 

◎DatabaseHelper.java

package com.example.samplecontentprovider;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;

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(@Nullable 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 객체를 생성하도록 작성해준다.

 

◎PersonProvider.java

package com.example.samplecontentprovider;

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 android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class PersonProvider extends ContentProvider {
    private static final String AUTHORITY = "com.example.samplecontentprovider";
    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;

    /** UriMatcher -> 내용 제공자를 만들기 위한 고유 값의 content URL 생성
     * content://com.example.samplecontentprovider/person/1
     *
     * content:// = 내용 제공자에 의해 제어되는 데이터라는 의미로 항상 content:// 로 시작함
     * Authority = 특정 내용 구분자를 구분하는 고유의 값
     * Base path = 요청할 데이터의 자료형을 결정함(테이블 이름)
     * ID = 맨 뒤의 1과 같은 숫자를 가리키며 요청할 데이터 레코드를 결정함
     * */

    @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_ID + " ASC");
                break;
            default:
                throw new IllegalArgumentException("에러: " + uri);
        }
        cursor.setNotificationUri(getContext().getContentResolver(), uri);

        return cursor;
    }

    @Nullable
    @Override
    // MIME 타입을 알기 위해 호출
    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 객체 뒤에 id를 append
            Uri _uri = ContentUris.withAppendedId(CONTENT_URI, id);
            
            // ContentResolver 에게 기존 DB의 내용이 변경되었다는 것을 알리는 역할을 한다.
            // ContentProvider 구현자는 notifyChange 를 사용하여 DB의 변경 사항을 알리는 것이 좋다. 
            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;
    }
}

 

직접 만든 내용 제공자이기 때문에 해당 provider는 DatabaseHelper를 사용하여 CRUD 메서드를 직접 정의하였다. 

또한 Read를 제외한 모든 동작은 해당 provider를 구현받는 resolver객체에게 DB의 변경 사항을 nofityChange로 알려줘야 한다.

 

이제 MainActivity에서 ContentResolver객체를 참조하여 CRUD 동작을 수행할 수 있게끔 코드를 작성해주면 된다.

 

◎MainActivity.java

package com.example.samplecontentprovider;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

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 button = findViewById(R.id.button1);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                insertPerson();
            }
        });

        Button button2 = findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                queryPerson();
            }
        });

        Button button3 = findViewById(R.id.button3);
        button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                updatePerson();
            }
        });

        Button button4 = findViewById(R.id.button4);
        button4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                deletePerson();
            }
        });
    }

    public void insertPerson() {
        println("insertPerson 호출됨");

        String uriString = "content://com.example.samplecontentprovider/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());
    }

    public void queryPerson() {
        try {
            String uriString = "content://com.example.samplecontentprovider/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()) {
                @SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex(columns[0]));
                @SuppressLint("Range") int age = cursor.getInt(cursor.getColumnIndex(columns[1]));
                @SuppressLint("Range") 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://com.example.samplecontentprovider/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://com.example.samplecontentprovider/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);
    }


    public void println(String data) {
        textView.append(data + "\n");
    }
}

MainActivity에서는 내용제공자의 uri 스트링을 따서 그대로 ContentResolver객체를 불러와 CRUD 동작을 수행한다.

 


 

Comments