프로그래밍/안드로이드

안드로이드 - Content Provider (콘텐트 프로바이더)

가카리 2015. 11. 29. 22:01
반응형

 

1. URI

 

안드로이드의 보안 정책상 응용 프로그램이 만든 데이터는 기본적으로 혼자만 액세스 가능하다. 자신이 만든 데이터를 외부로 공개할 때는 Content Provider를 제공해야한다.

 

콘텐트 프로바이더는 응용 프로그램을 구성하는 컴포넌트 중 하나로서 데이터를 제공하는 역할을 한다.

 

URI(Uniform Resource Identifier)는 웹상의 주소를 나타내는 URL보다 더 상위의 개념이다. 국제 표준에 URI를 작성하는 방식은 다음과 같이 명시되어 있다.

 

    content://authority/path/id

 

content://는 이 문자열이 URI임을 나타내는 접두이며 무조건 붙여야 한다. authority는 정보 제공자의 명칭이되 중복되면 안 되므로 패키지명을 사용할 것을 권장한다.

 

path는 정보의 종류를 지정하는 가상의 경로이다. id는 어떤 정보를 원하는지 지정하되 전체 정보를 다 읽을 때는 생략할 수 있다.

 

CP는 단수와 복수에 대해 두 가지 형태의 URI를 각각 정의해야 한다.

 

id까지만 있으면 단수이고 path 까지만 있으면 복수이다. 다음은 URI의 몇 가지 예로서 stockmarket이라는 가상의 회사에서 제공하는 정보의 예이다.

 

    content://com.stockmarket/stock //주식 정보

    content://com.stockmarket/stock/posco //posco 종목의 주식 정보

 

URI는 항상 content://로 시작하고 이어서 정보 제공자를 밝히는 저작권 정보가 온다. com.stockmarket이라는 자사의 URL을 사용했으므로 웬만해서는 제공자가 중복되지 않을 것이다.

 

정보 제공자와 정보의 종류에 따라 이 형식대로 URI 문자열을 만든 후 다음 정적 메소드로 URI 객체를 생성한다.

 

    static Uri parse(String uriString)

 

이 메소드는 성능상의 문제로 인해 에러 처리는 하지 않는다. 즉 URI를 잘못 작성하면 쓰레기값이 조사된다.

 

다음 메소드는 URI의 path 정보를 문자열 목록으로 조사한다.

 

    List<String> getPathSegments()

 

이 목록의 0번째 요소가 path이며 1번째 요소가 id이되 /를 제외한 문자열만 조사된다.

 

문자열을 비교해서는 속도가 느리므로 UriMatcher라는 유틸리티 클래스를 이용해서 쉽게 문자열을 비교 할 수 있다.

 

    void addURI(String authority, String path, int code)

    int match(Uri uri)

 

addURI메소드로 authority, path의 쌍으로 정수 코드와 대응시켜 맵을 등록한다. path 에서 *는 임의의 문자열과 대응되며 #은 숫자 하나와 대응된다.

match 메소드는 uri를 분석하여 등록된 정수 코드를 리턴한다. 만약 uri에 해당하는 코드가 발견되지 않으면 -1을 리턴한다.

 

CP는 클라이언트의 URI를 일일이 분석할 필요없이 UriMatcher가 분석해 놓은 정수 코드로 요청을 파악하여 정보를 리턴하면 된다.

정수로부터 요청을 구분할 수 있으므로 switch case문으로 분기할 수 있어 편리하고 코드도 깔끔하게 정리된다.

 

2. 자료공유

 

CP를 만들기 위해서 먼저 ContentProvider클래스를 상속받아야 하며 정보를 관리 및 제공하는 메소드를 재정의해야 한다.

 

onCreate는 CP가 로드될 때 호출되는데 여기서 제공할 데이터를 준비한다.

 

예를 들어 데이터베이스에 들어 있는 정보라면 onCreate에서 DB를 열어 두면 된다. 다음 메소드는 제공하는 데이터의 MIME 타입을 조사한다.

 

    String getType(Uri uri)

 

대체로 다음 형식대로 작성한다.

 

    1. 단수 : vnd.회사명.cursor.item/타입

    2. 복수 : vnd.회사명.cursor.dir/타입

 

CP는 원래 기능을 그대로 유지한 채로 고유의 데이터를 외부에 공개하는 컴포넌트를 하나 더 추가하는 것이므로 기존의 액티비티는 전혀 건드릴 필요가 없다.

 

1. CP를 만드는 예제

 

위와 같이 3부분을 수정해야 합니다.

 

AndroidManifest.xml

application 태그 아래, Provider 태그를 추가한다. 시스템은 정보를 요청하는 쪽의 URI와 매니페스트의 URI를 비교해보고 제공자가 일치하는 CP를 호출한다.

 

그리고 반드시 퍼미션을 정의해줘야 한다. 안그러면 Permission denial: opening provider 에러가 발생한다. (다음 링크 참고 : http://stackoverflow.com/questions/14368867/permission-denial-opening-provider)

 

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.example.ch20_cp_use"

android:versionCode="1"

android:versionName="1.0" >

 

<uses-sdk

android:minSdkVersion="19"

android:targetSdkVersion="19" />

<!-- ContentProvider 구현한 앱에서 정의한 퍼미션을 그대로 써줘야한다. -->

<uses-permission android:name="de.test.READ_DATABASE" />

    <uses-permission android:name="de.test.WRITE_DATABASE" />

 

<application

android:allowBackup="true"

android:icon="@drawable/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:name=".CallWordCP"

android:label="@string/app_name" >

<intent-filter>

<action android:name="android.intent.action.MAIN" />

 

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

</intent-filter>

</activity>

 

</application>

 

 

 

</manifest>

 

 

englishword.xml

 

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    >

<Button

    android:id="@+id/insert"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Insert"

    />

<Button

    android:id="@+id/delete"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Delete"

    />

<Button

    android:id="@+id/update"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Update"

    />

<Button

    android:id="@+id/select"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Select"

    />

<EditText

    android:id="@+id/edittext"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    />

</LinearLayout>

 

 

EnglishWord.java

 

package com.example.ch20_contentprovider;

 

import android.app.*;

import android.content.*;

import android.database.*;

import android.database.sqlite.*;

import android.os.*;

import android.view.*;

import android.widget.*;

 

public class EnglishWord extends Activity {

    WordDBHelper mHelper;

    EditText mText;

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.englishword);

 

        mHelper = new WordDBHelper(this);

        mText = (EditText)findViewById(R.id.edittext);

    }

    

    public void mOnClick(View v) {

        SQLiteDatabase db;

        ContentValues row;

        switch (v.getId()) {

        case R.id.insert:

            db = mHelper.getWritableDatabase();

            // insert 메서드로 삽입

            row = new ContentValues();

            row.put("eng", "boy");

            row.put("han", "머스마");

            db.insert("dic", null, row);

            // SQL 명령으로 삽입

            db.execSQL("INSERT INTO dic VALUES (null, 'girl', '가시나');");

            mHelper.close();

            mText.setText("Insert Success");

            break;

        case R.id.delete:

            db = mHelper.getWritableDatabase();

            // delete 메서드로 삭제

            db.delete("dic", null, null);

            // SQL 명령으로 삭제

            //db.execSQL("DELETE FROM dic;");

            mHelper.close();

            mText.setText("Delete Success");

            break;

        case R.id.update:

            db = mHelper.getWritableDatabase();

            // update 메서드로 갱신

            row = new ContentValues();

            row.put("han", "소년");

            db.update("dic", row, "eng = 'boy'", null);

            // SQL 명령으로 갱신

            //db.execSQL("UPDATE dic SET han = '소년' WHERE eng = 'boy';");

            mHelper.close();

            mText.setText("Update Success");

            break;

        case R.id.select:

            db = mHelper.getReadableDatabase();

            Cursor cursor;

            // query 메서드로 읽기

            //cursor = db.query("dic", new String[] {"eng", "han"}, null,

            //        null, null, null, null);

            // SQL 명령으로 읽기

            cursor = db.rawQuery("SELECT eng, han FROM dic", null);

        

            String Result = "";

            while (cursor.moveToNext()) {

                String eng = cursor.getString(0);

                String han = cursor.getString(1);

                Result += (eng + " = " + han + "\n");

            }

 

            if (Result.length() == 0) {

                mText.setText("Empyt Set");

            } else {

                mText.setText(Result);

            }

            cursor.close();

            mHelper.close();

            break;

        }

    }

}

 

//여기서 SQLite 데이터 베이스를 만든다.

class WordDBHelper extends SQLiteOpenHelper {

    public WordDBHelper(Context context) {

        super(context, "EngWord.db", null, 1);

    }

 

    public void onCreate(SQLiteDatabase db) {

        db.execSQL("CREATE TABLE dic ( _id INTEGER PRIMARY KEY AUTOINCREMENT, " +

        "eng TEXT, han TEXT);");

    }

 

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        db.execSQL("DROP TABLE IF EXISTS dic");

        onCreate(db);

    }

}

 

 

EWProvider.java

 

package com.example.ch20_contentprovider;

 

import android.content.ContentProvider;

import android.content.ContentUris;

import android.content.ContentValues;

import android.content.UriMatcher;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

import android.net.Uri;

import android.text.TextUtils;

 

public class EWProvider extends ContentProvider{

    static final Uri CONTENT_URI = Uri.parse("content://com.example.ch20_contentprovider/word");

    

    static final int ALLWORD = 1;

    static final int ONEWORD = 2;

    

    static final UriMatcher Matcher;

    static {

        Matcher = new UriMatcher(UriMatcher.NO_MATCH);

        Matcher.addURI("com.example.ch20_contentprovider", "word", ALLWORD);

        Matcher.addURI("com.example.ch20_contentprovider", "word/*", ONEWORD);

    }

    

    SQLiteDatabase mDB;

    

    //콘텐트 프로바이더 객체가 생성되면 호출됨.

    @Override

    public boolean onCreate() {

        // TODO Auto-generated method stub

        //공유할 데이터베이스를 열어둔다.

        WordDBHelper helper = new WordDBHelper(getContext());

        mDB = helper.getWritableDatabase();

        

        return true;

    }

    

    //MIME 타입을 정의해둠

    //클라리언트에서 주어진 uri 보고 ContentProvider 제공하는 데이터의 MIME타입을 리턴해준다.

    //특정한 상황에서만 사용

    @Override

    public String getType(Uri uri) {

        // TODO Auto-generated method stub

        if(Matcher.match(uri) == ALLWORD){

            return "vnd.EnglishWord.andexam.cursor.item/word";

        }

        if(Matcher.match(uri) == ONEWORD){

            return "vnd.EnglishWord.andexam.cursor.dir/word";

        }

        

        return null;

    }

 

    //실제 데이터를 제공하는 부분

    @Override

    public Cursor query(Uri uri, String[] projection, String selection,

            String[] selectionArgs, String sortOrder) {

        // TODO Auto-generated method stub

        String sql;

        

        //전체에 대한 쿼리 명령

        sql = "SELECT eng, han FROM dic";

        

        //단어 선택 where 추가

        //인수로 전달된 uri 분석함

        if(Matcher.match(uri) == ONEWORD){

            sql += " where eng = '" + uri.getPathSegments().get(1) + "'";//get(1)에서 1 id 의미

        }

        

        Cursor cursor = mDB.rawQuery(sql, null);//sql 실행

        

        return cursor;

    }

 

    // 하나를 삽입한다.

    public Uri insert(Uri uri, ContentValues values) {

        long row = mDB.insert("dic", null, values);

        

        //입력에 성공하면 notifyChange 메소드를 호출함.

        if (row > 0) {

            //아래는 추가된 ID 보유한 Uri 객체를 리턴하는 메소드

            //관련 링크 : http://posnopi13.tistory.com/19 참고

            Uri notiuri = ContentUris.withAppendedId(CONTENT_URI, row);

            

            //다른 CP들에게 변화를 알림

            getContext().getContentResolver().notifyChange(notiuri, null);

            return notiuri;

        }

        return null;

    }

 

    //

    public int delete(Uri uri, String selection, String[] selectionArgs) {

        int count = 0;

        

        //단수와 복수에 대한 요청 구분

        switch (Matcher.match(uri)) {

        case ALLWORD://복수 요청의 경우

            count = mDB.delete("dic", selection, selectionArgs);

            break;

        case ONEWORD://단수 요청의 경우

            String where;

            //해당하는 것만 지우기 위해 쿼리문을 만듬

            where = "eng = '" + uri.getPathSegments().get(1) + "'";

            //String null체크를 하기위한 메소드 빈문자열경우true 리턴

            if (TextUtils.isEmpty(selection) == false) {

                where += " AND" + selection;

            }

            count = mDB.delete("dic", where, selectionArgs);

            break;

        }

        

        //변화된것을 알림

        getContext().getContentResolver().notifyChange(uri, null);

        return count;

        //*/

        

        /* 아래와같이 구현해도됨

        String sql;

        

        // 전체에 대한 쿼리 명령

        sql = "DELETE FROM dic";

        

        // 단어 선택 where 추가

        if (Matcher.match(uri) == ONEWORD) {

            sql += " where eng = '" + uri.getPathSegments().get(1) + "'";

        }

        mDB.execSQL(sql);

        return 1;

        //*/

    }

 

    public int update(Uri uri, ContentValues values, String selection,

            String[] selectionArgs) {

        int count = 0;

        

        switch (Matcher.match(uri)) {

        case ALLWORD:

            count = mDB.update("dic", values, selection, selectionArgs);

            break;

        case ONEWORD:

            String where;

            where = "eng = '" + uri.getPathSegments().get(1) + "'";

            //delete쪽과 같음

            if (TextUtils.isEmpty(selection) == false) {

                where += " AND " + selection;

            }

            count = mDB.update("dic", values, where, selectionArgs);

            break;

        }

        

        getContext().getContentResolver().notifyChange(uri, null);

        return count;

    }

}

 

실행 화면

insert 버튼을 누르면 데이터베이스에 추가되고 delete버튼을 누르면 데이터베이스에서 삭제된다.

그리고 select버튼을 누르면 값이 출력된다. update는 머스마를 소년으로 바꾼다.

 

2. CP를 사용하는 예제

 

AndroidManifest.xml

 

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.example.ch20_cp_use"

android:versionCode="1"

android:versionName="1.0" >

 

<uses-sdk

android:minSdkVersion="19"

android:targetSdkVersion="19" />

<!-- ContentProvider 구현한 앱에서 정의한 퍼미션을 그대로 써줘야한다. -->

<uses-permission android:name="de.test.READ_DATABASE" />

    <uses-permission android:name="de.test.WRITE_DATABASE" />

 

<application

android:allowBackup="true"

android:icon="@drawable/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:name=".CallWordCP"

android:label="@string/app_name" >

<intent-filter>

<action android:name="android.intent.action.MAIN" />

 

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

</intent-filter>

</activity>

 

</application>

 

 

 

</manifest>

 

 

callwordcp.xml

 

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    >

<Button

    android:id="@+id/readall"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Readl All"

    />

<Button

    android:id="@+id/readone"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Read One"

    />

<Button

    android:id="@+id/insert"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Insert"

    />

<Button

    android:id="@+id/delete"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Delete"

    />

<Button

    android:id="@+id/update"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:onClick="mOnClick"

    android:text="Update"

    />

<EditText

    android:id="@+id/edittext"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    />

</LinearLayout>

 

 

CallWordCP.java

 

package com.example.ch20_cp_use;

 

import android.app.Activity;

import android.content.ContentResolver;

import android.content.ContentValues;

import android.database.Cursor;

import android.net.Uri;

import android.os.Bundle;

import android.view.View;

import android.widget.EditText;

 

public class CallWordCP extends Activity {

    static final String WORDURI = "content://com.example.ch20_contentprovider/word";

    EditText mText;                

    

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.callwordcp);

        

        mText = (EditText)findViewById(R.id.edittext);

    }

    

    public void mOnClick(View v){

        

        //먼저 ContentResolver 객체를 구한다.

        //리졸버를 쿼리를 통해 cp 간접적으로 통신하는 표준 인터페이스이다.

        ContentResolver cr = getContentResolver();

        

        switch(v.getId()){

        //전부 읽기

        case R.id.readall:

            Cursor cursor = cr.query(Uri.parse(WORDURI), null, null, null, null);

        

            String Result = "";

            //데이터 베이스 값을 가져와서 뿌려줌

            while(cursor.moveToNext()){

                String eng = cursor.getString(0);

                String han = cursor.getString(1);

                Result += (eng + " = " + han + "\n");

            }

            

            if(Result.length() == 0){

                mText.setText("Empty set");

            }else{

                mText.setText(Result);

            }

            

            cursor.close();

            break;

            

            //하나만 읽기

            case R.id.readone:

                // /boy인것만 읽어옴

                Cursor cursor2 = cr.query(Uri.parse(WORDURI + "/boy"),

                        null, null, null, null);

                

                String Result2 = "";

                if(cursor2.moveToFirst()){

                    String eng = cursor2.getString(0);

                    String han = cursor2.getString(1);

                    Result2 += (eng + " = " + han + "\n");

                }

                

                if(Result2.length() == 0){

                    mText.setText("Empty Set");

                }else{

                    mText.setText(Result2);

                }

                cursor2.close();

                break;

                

            //삽입

            case R.id.insert:

                ContentValues row = new ContentValues();

                row.put("eng", "school");

                row.put("han", "학교");

                cr.insert(Uri.parse(WORDURI), row);

                mText.setText("Insert Success");

                break;

                

            //삭제

            case R.id.delete:

                cr.delete(Uri.parse(WORDURI), null, null);

                mText.setText("Delete Success");

                break;

                

            //수정

            case R.id.update:

                ContentValues row2 = new ContentValues();

                row2.put("han", "핵교다");

                cr.update(Uri.parse(WORDURI + "/school"), row2, null, null);

                mText.setText("Update Success");

                break;

        }

        

    }

    

 

}

 

 

실행 화면

CP를 구현한 예제의 데이터베이스 값을 가져오거나 수정할 수 있다.

즉, 첫번째 앱에 있는 데이터베이스 dic를 두번째 앱인 CP_USE앱에서 수정할 수 있다는 의미이다.

 

반응형