본문 바로가기

안드로이드

내용 제공자

반응형

내용 제공자는 컨텐트 프로바이더라고도 부르며, 한 앱에서 관리하는 데이터를 다른 앱에서도 접근할 수 있도록 해준다

 

내용 제공자도 앱 구성요소이기 때문에 시스템에서 관리하며, 매니페스트 파일에 등록해야 사용할 수 있다

 

내용 제공자가 필요한 이유는 앱의 보안 때문이다

다른 사람이 만든 앱이 나의 앱의 데이터를 마음대로 바꾸면 안되기 때문이다

그래서 각 앱은 자신의 프로세스 권한 안에서만 데이터에 접근할 수 있도록 되어 있다

 

A라는 앱과 B라는 앱은 각각 독립된 프로세스를 가지고 있으며, A는 A의 데이터를, B는 B의 데이터만 사용해야 한다

하지만 가끔은 서로 다른 앱의 데이터에 접근해야 하는 경우, 내용 제공자를 사용하게 되면, 다른 앱에게 데이터 접근 통로를 열어줄 수 있다

주의할 점은 반드시 허용된 통로로만 접근해야 한다는 것이다

 

내용 제공자에서 공유할 있는 데이터들은

데이터베이스, 파일, SharedPreferences

이 3가지가 있다

 

이 중에서 데이터베이스에 접근하는 것이 가장 일반적인데, 내용 제공자는 CRUD 동작을 기준으로 하고 있기 때문이다

내용 제공자에서 허용한 통로로 접근하려면 ContentResolver 객체가 필요하다

 

 

AndoridManifest

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

    <!-- 권한을 새로 정의 -->
    <uses-permission
        android:name="com.example.a62_sample_provider.READ_DATABASE"
        android:protectionLevel="normal" />
    <uses-permission
        android:name="com.example.a62_sample_provider.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.A62_Sample_Provider">

        <!-- authorities는 내용 제공자를 정의할때 설정한 authority의 값과 동일하게 설정 -->
        <!-- name 속성 값으로는 내용 제공자 클래스인 PersonProvider 클래스를 설정 -->
        <provider
            android:name=".PersonProvider"
            android:authorities="com.example.a62_sample_provider"
            android:exported="true"
            android:readPermission="com.example.a62_sample_provider.READ_DATABASE"
            android:writePermission="com.example.a62_sample_provider.WRITE_DATABASE" />

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

 

 

activity_main

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="insert" />

        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="query" />

        <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="update" />

        <Button
            android:id="@+id/button4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="delete" />
    </LinearLayout>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/textView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="TextView" />
        </LinearLayout>
    </ScrollView>
</LinearLayout>

 

 

DatabaseHelper.java

package com.example.a62_sample_provider;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;


// person.db 데이터베이스 만들고, person 테이블 생성
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);
    }
}

 

 

PersonProvider.java

package com.example.a62_sample_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 = "com.example.a62_sample_provider";
    private static final String base_path = "person";
    //    내용 제공자를 만들기 위해서는 고유한 값을 가진 content URI를 만들어야한다. 지금은 패키지 이름과 person 테이블의 이름을 합쳐서 만들었다
    public static final Uri content_uri = Uri.parse("content://" + authority + "/" + base_path);
//    cotent URI를 정의하는 형식 - content://com.example.a62_sample_provider/person/1
//    conten:// - 내용 제공자에 의해 제어되는 데이터라는 의미로 항상 content:// 로 시작함
//    authority - 지금은 패키지 이름을 가리키며, 특정 내용 제공자를 구분하는 고유한 값
//    base_path - 지금은 person을 가리키며, 요청할 데이터의 자료형을 결정함. 여기서는 테이블 이름
//    id - 맨 뒤의 1과 같은 숫자를 가리키며 요청할 데이터 레코드를 지정함

    private static final int persons = 1;
    private static final int person_id = 2;

    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    //    UriMatcher 객체는 URI르 매칭하는데 사용
    //    match 메소드를 호출하면 UriMatcher에 AddURI 메소드를 이용해 추가된 URI 중에서 실행 가능한 것이 있는지 확인한다
    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;
    }

//    Provider 클래스에는 insert, query, update, delete 메소드들을  재정의하였다
//    이 메소드들은 ContentResolver 객체에 있는 메소드들이다.
//    액티비티에서 getContentResolver 메소드를 호출하면 ContentResolver 객체를 반환하는데
//    이 객체에는 query, insert, update, delete 등의 메소드가 정의되어 있어 내용 제공자의 URI를 파라미터로 전달하면서
//    데이터를 조회, 추가, 수정, 삭제하는 일이 가능하다

    @Nullable
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
//        파라미터 1 - URK, 2 - 어떤 칼럼들을 조회할 것인지 지정, null을 전달하면 모든 칼럼 조회
//        3 - sql에서 where 절에 들어갈 조건 지정, null이면 where절이 없음
//        4 - 3번 파라미터에 값이 있을 경우, 그 안에 들어갈 조건 값
//        5 - 정렬 칼럼을 지정, null이면 정렬이 적용되지 않음

        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("알 수 없는 URI " + uri);
        }

        cursor.setNotificationUri(getContext().getContentResolver(), uri);

        return cursor;
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values) {
//        파라미터 1 - URI, 2- 저장할 칼럼명과 값들이 들어간 ContentValues 객체

        long id = database.insert(DatabaseHelper.table_name, null, values);

        if (id > 0) {
            Uri _uri = ContentUris.withAppendedId(content_uri, id);

//            notifyChange - 레코드가 추가, 수정, 삭제 되었을 때 변경이 일어났음을 알려주는 메소드
            getContext().getContentResolver().notifyChange(_uri, null);

//            새로 추가된 값의 Uri 정보 반환
            return _uri;
        }

        throw new SQLException("추가 실패 -> URI : " + uri);
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
//        파라미터 1 - URI, 2 - 저장할 칼럼명과 값들이 들어간 ContentValues 객체, null이면 안된다는 점에 주의
//        3 - where 절에 들어갈 조건, null이면 where 절 없음
//        4 - 3번 파라미터에 값이 있을 경우, 그 안에 들어갈 조건 값

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


    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
//        파라미터 1 - URI, 2 - where 절에 들어갈 조건, null이면 where 절 없음
//        3 - 2번 파라미터에 값이 있을 경우, 그 안에 들어갈 조건 값

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


    @Nullable
    @Override
    public String getType(Uri uri) {
//        MIME 타입이 무엇인지 알고 싶을때 사용하는 메소드
//        URI 객체가 파라미터로 전달되며 결과 값으로 MIME 타입이 반환
//        MIME 타입을 알 수 없을 경우에는 null 값이 반환

        switch (uriMatcher.match(uri)) {
            case persons:
                return "vnd.android.cursor.dir/persons";

            default:
                throw new IllegalArgumentException("알 수 없는 URI " + uri);
        }
    }
}

 

 

 

MainActivity

package com.example.a62_sample_provider;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
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 button = findViewById(R.id.button);
        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 println(String data) {
        textView.append(data + "\n");
    }

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

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

    public void queryPerson() {
        println("queryPerson 호출");

        try {
//            문자열에서 Uri 객체를 만들 때는 new 연산자를 이용해 Uri.Builder 객체를 만든 후
//            build와 parse 메소드를 호출하면서 문자열을 파라미터로 전달하면 된다
            String uriString = "content://com.example.a62_sample_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() {
        println("updatePerson 호출");

        String uriString = "content://com.example.a62_sample_provider/person";
        Uri uri = new Uri.Builder().build().parse(uriString);

//        where 조건의 문자열에 mobile = ?를 넣었다
//        ? 기호는 selectionArgs 배열 변수의 첫 번째 원소로 대체된다
//        따라서 where 조건은 mobile = 010-1000-1000이 된다
        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() {
        println("deletePerson 호출");

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


}

 

 

 

 

반응형