顯示具有 ContentProvider 標籤的文章。 顯示所有文章
顯示具有 ContentProvider 標籤的文章。 顯示所有文章

2011年12月28日 星期三

Android - ContentProvider - Sqlite應用 (1)

對於ContentProvider的原理,請看原文:
http://developer.android.com/guide/topics/providers/content-providers.html

網 路上也有很多人解釋ContentProvider,這裡不多說。我的實作測試程式和網路上不同,從Android Source Code可以發現/packages/providers底下有很多個資料夾。這些大部份都不會有Acitvity,因為這類型的apk單純就只是一個介 面給其它的apk呼叫用的。所以如果實作程式將Content Provider和含Activity合成同一支apk,就比較感覺不到效果。因此測試含有2支apk

(1)MyDataBaseProvider.apk:內含2個java檔
     1. MyContentProvider.java : 繼承ContentProvider,需要實作6個function
         query()
         insert()
         update()
         delete()
         getType()
         onCreate()  
     2. MyOpenHelper.java : 繼承 SQLiteOpenHelper,這是為了方便產生DB用途

(2)UseProvider.apk :裡面有一個Main.java。這支apk和MyDataBaseProvider分開獨立的。利用系統的ContentResolver找出適合的ContentProvider溝通(本範例也就是存取DB資料)

理論上,應該是先有ContentProvider介面設計好之後,未來有需要的apk就都可以透過它去存取資料。

MyContentProvider.java的程式碼:
public class MyContentProvider extends ContentProvider {
    private MyOpenHelper myOpenHelper;
    private static final String[] MYCOLUMN = new String[]{ "mycolumn"};
   
    @Override
    public boolean onCreate() {
        myOpenHelper = new MyOpenHelper(getContext());
        return true;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        SQLiteDatabase myDatabase = myOpenHelper.getWritableDatabase();
        Cursor c = myDatabase.query(uri.getLastPathSegment(), MYCOLUMN, null, null, null, null, null);    
        return c;
    }
    @Override
    public String getType(Uri uri) {
        return null;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }
    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        return 0;
    }

}

其中的getLastPathSegment目的是取出uri最後一個字段,有可能是Table的名稱也有可能是單筆資料的row id。因此可以確保異動的資料對象到底是整張Table還是某一筆資料。

MyOpenHelper.java的程式碼:
public class MyOpenHelper extends SQLiteOpenHelper {

    // 定義DB相關資訊
    private static final String DATABASE_NAME = "MyDatabase";
    private static final int DATABASE_VERSION = 1;
    private static final String TABLE_NAME = "MyTable";

    public MyOpenHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 這一個onCreate是一個callback function
        // 之後程式呼叫到SQLiteOpenHelper裡的getWritableDatabase()時,
        // 會檢查有無DB,沒有就先create "MyDatabase"的DB
        // 然後再檢查是否要callback這個onCreate()而產生一個"MyTable"的table,並塞入一筆資料
        db.execSQL("CREATE TABLE " + TABLE_NAME
                + " (id integer primary key autoincrement, mycolumn text);");
        String mySql = "insert into " + TABLE_NAME
                + " (mycolumn) values('gill');";
        db.execSQL(mySql);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 如果DB版本更新時需要進行什麼動作,就寫在這裡
    }
}

AndroidManifest.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.test.mydatabaseprovider"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <provider android:name="MyContentProvider"
            android:authorities="com.gill.test.authorities">
        </provider>   
    </application>
</manifest>

這邊的authorities值隨便你取,其它apk要透過這個authorities和要存取的table name組出一個URI,才可以存取DB內該table資料。

public class Main extends Activity {
    // AUTHORITY要看該Provider的AndroidManifest.xml中的定義
    public static final String AUTHORITY = "com.gill.test.authorities";
    // URI = AUTHORITY再加table name
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
            + "/MyTable");
    private static final String[] MYCOLUMN = new String[] { "mycolumn" };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // 透過getContentResolver由系統依據uri,決定要執行那一個ContentProvider
        // 所以這裡不用直接呼叫MyDataBaseProvider也可以取得資料
        Cursor myCursor = getContentResolver().query(CONTENT_URI, MYCOLUMN, null,
                null, null);
       
        myCursor.moveToFirst(); //一定要寫,不然會出錯
        TextView txt = (TextView) findViewById(R.id.txt);
        txt.setText(myCursor.getString(0));
    }
}

安裝這兩支apk後,執行UseMyProvider,畫面如下:

gill是來自於DB




















如果想知道DB位置和裡面的table及資料,可以利用adb shell進入後,
# cd data/data/com.test.mydatabaseprovider/databases
# ls    
MyDatabase
# sqlite3 MyDatabase
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .table
MyTable           android_metadata
sqlite> .header on
sqlite> select * from MyTable;
id|mycolumn
1|gill

就可以看到欄位名稱和裡面的值了。

Android - ContentProvider - Sqlite應用(4) 注意事項

即使是同一支apk中,如果想要擁有兩個ContentProvider,要注意兩件事情。

(1) 不同ContentProvider的DB 名稱不可相同
(2) 不同ContentProvider的AUTHORITY不可相同

否則會無法正常create出DB。
換言之,ContentProvider的處理對象是整個DB而不是單一張Table,因此要遵循上述兩項條件。

Android - ContentProvider - Sqlite應用 (3) ContentObserver

Android 提供一種「監聽者」的機制,利用ContentObserver可以隨時監聽某個Content是否有變化。我們對先前的Sqlite的DB裡面的 Table進行監聽,一旦新增資料時,就會呼叫秀出Log.d()。使用ContentObserver主要步驟如下:
(1) MyContentObserver.java:實作繼承ContentObserver,並完成
    public MyContentObserver(Handler handler) {
        super(handler);
    }
    public void onChange(boolean selfChange) {
        //當增加一筆資料時,想要做任何事,就寫在這裡
        Log.d("MyContentObserver", "Data insert!");
    }

(2) MyContentProvider.java中: 在query() 裡增加c.setNotificationUri();另外在insert() 裡增加getContext().getContentResolver().notifyChange(); 如此一旦資料新增時,就會去利用callback function呼叫到MyContentObserver裡面的onChange()

(3)Main.java: 利用registerContentObservers() 和 unregisterContentObservers()進行監聽資料和取消監聽資料的動作

程式碼如下:
MyContentObserver.java (和Main是放在同一支apk)
public class MyContentObserver extends ContentObserver {
    public MyContentObserver(Handler handler) {
        super(handler);
    }
    public void onChange(boolean selfChange) {
        Log.d("MyContentObserver", "Data is changed!");
    }
}


public class MyContentProvider extends ContentProvider {

    private MyOpenHelper myOpenHelper;
    private SQLiteDatabase myDB;

    public static final String AUTHORITY = "com.gill.test.authorities";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
            + "/MyTable");
    private static final String[] MYCOLUMN = new String[] { "mycolumn" };

    @Override
    public boolean onCreate() {
        myOpenHelper = new MyOpenHelper(getContext());
        myDB = myOpenHelper.getWritableDatabase();
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        SQLiteDatabase myDatabase = myOpenHelper.getWritableDatabase();
        Cursor c = myDatabase.query(uri.getLastPathSegment(), MYCOLUMN, null,
                null, null, null, null);
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        long rowId = -1;

        //第二個參數隨便是該table內的任一個欄位,反正insert就是一整筆下去新增,系統會自動配對欄位和values
        rowId = myDB.insert(uri.getLastPathSegment(), "mycolumn", values);
        Uri newUri = Uri.withAppendedPath(CONTENT_URI, "" + rowId);
        getContext().getContentResolver().notifyChange(uri, null);
        return newUri;
    }
}

Main.java
public class Main extends Activity {
    private ContentResolver mResolver;
    private Cursor myCursor;
    private TextView txt;
    private EditText edit;
    private MyContentObserver myObserver;
    private Handler handler = new Handler();
   
    // AUTHORITY要看該Provider的AndroidManifest.xml中的定義
    public static final String AUTHORITY = "com.gill.test.authorities";

    // URI = AUTHORITY再加table name
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
            + "/MyTable");

    private static final String[] MYCOLUMN = new String[] { "mycolumn" };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mResolver = getContentResolver();

        // 透過getContentResolver由系統依據uri,決定要執行那一個ContentProvider
        // 所以這裡不用直接呼叫MyDataBaseProvider也可以取得資料
        myCursor = mResolver.query(CONTENT_URI, MYCOLUMN, null, null, null);

        myCursor.moveToFirst(); // 一定要寫,不然會出錯
        txt = (TextView) findViewById(R.id.txt);
        txt.setText(myCursor.getString(0));

        edit = (EditText) findViewById(R.id.edit);
        Button insert_btn = (Button) findViewById(R.id.insert);


        // 新增
        OnClickListener insert_lis = new OnClickListener() {
            public void onClick(View v) {
                ContentValues values = new ContentValues();
                // 如果table有很多欄位,就在這裡把所有的欄位利用values.put補齊
                values.put("mycolumn", edit.getText().toString());
                mResolver.insert(CONTENT_URI, values);
                txt.setText(myCursor.getString(0));
            }
        };
        insert_btn.setOnClickListener(insert_lis);
   
        registerContentObservers();
    }

    @Override
    protected void onStop() {
      super.onStop();
      unregisterContentObservers();
    }
   
    //註冊監聽
    private void registerContentObservers() {
        myObserver = new MyContentObserver(handler);
        getContentResolver().registerContentObserver(CONTENT_URI, true, myObserver);
    }

    //取消監聽
    private void unregisterContentObservers() {
        if (myObserver != null) {
            getContentResolver().unregisterContentObserver(myObserver);
            myObserver = null;
        }
    }

}

注 意registerContentObserver()第二個參數是true,表示對這個CONTENT_URI底下所有的uri都進行監聽。舉例如果該 值是true,則content://com.gill.test.authorities/MyTable/1的變化,也會監聽。這裡的1是指 table裡面的_id(也有可能是2、3、…等)。如果是false的話,就只對 content://com.gill.test.authorities/MyTable監聽,那就達不到我們要的效果了。content: //com.gill.test.authorities/MyTable就叫Base Uri,而content://com.gill.test.authorities/MyTable/1這種的就是Base Uri的descendanted uri。請參考原文:http://developer.android.com/reference/android/content/ContentResolver.html#registerContentObserver%28android.net.Uri,%20boolean,%20android.database.ContentObserver%29

Android - ContentProvider - Sqlite應用 (2) 新增/修改/刪除/上一筆/下一筆

依前文的程式,繼續增加其它功能,使其擁有最基本的資料庫管理功能
(1)新增資料
(2)修改資料
(3)刪除資料
(4)查詢上一筆資料
(5)查詢下一筆資料

注意,本程式單純範例,因此完全不考慮requery及cursor move超出陣列空間的問題。實際撰寫程式時,需調整程式避免錯誤發生。

MyContentProvider.java 如下:
public class MyContentProvider extends ContentProvider {

    private MyOpenHelper myOpenHelper;
    private SQLiteDatabase myDB;

    public static final String AUTHORITY = "com.gill.test.authorities";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
            + "/MyTable");
    private static final String[] MYCOLUMN = new String[] { "mycolumn" };

    @Override
    public boolean onCreate() {
        myOpenHelper = new MyOpenHelper(getContext());
        myDB = myOpenHelper.getWritableDatabase();
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        SQLiteDatabase myDatabase = myOpenHelper.getWritableDatabase();
        Cursor c = myDatabase.query(uri.getLastPathSegment(), MYCOLUMN, null,
                null, null, null, null);
        return c;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        long rowId = -1;

        //第二個參數隨便是該table內的任一個欄位,反正insert就是一整筆下去新增,系統會自動配對欄位和values
        rowId = myDB.insert(uri.getLastPathSegment(), "mycolumn", values);
        Uri newUri = Uri.withAppendedPath(CONTENT_URI, "" + rowId);
        return newUri;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int del_count = myDB.delete(uri.getLastPathSegment(), selection,
                selectionArgs);
        return del_count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        int update_count = myDB.update(uri.getLastPathSegment(), values, selection, selectionArgs);
        return update_count;
    }

}


Main.java如下:
public class Main extends Activity {
    private ContentResolver mResolver;
    private Cursor myCursor;
    private TextView txt;
    private EditText edit;
   
    // AUTHORITY要看該Provider的AndroidManifest.xml中的定義
    public static final String AUTHORITY = "com.gill.test.authorities";

    // URI = AUTHORITY再加table name
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
            + "/MyTable");

    private static final String[] MYCOLUMN = new String[] {"mycolumn" };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mResolver = getContentResolver();
       
        // 透過getContentResolver由系統依據uri,決定要執行那一個ContentProvider
        // 所以這裡不用直接呼叫MyDataBaseProvider也可以取得資料
        myCursor = mResolver.query(CONTENT_URI, MYCOLUMN, null,
                null, null);
       
        myCursor.moveToFirst(); //一定要寫,不然會出錯
        txt = (TextView) findViewById(R.id.txt);
        txt.setText(myCursor.getString(0));

       
        edit = (EditText) findViewById(R.id.edit);
        Button insert_btn = (Button) findViewById(R.id.insert);
        Button update_btn = (Button) findViewById(R.id.update);
        Button delete_btn = (Button) findViewById(R.id.delete);
        Button previous_btn = (Button) findViewById(R.id.previous);
        Button next_btn = (Button) findViewById(R.id.next);

       
       
        //新增
        OnClickListener insert_lis = new OnClickListener(){
            public void onClick(View v) {
                ContentValues values = new ContentValues();
                //如果table有很多欄位,就在這裡把所有的欄位利用values.put補齊
                values.put("mycolumn", edit.getText().toString());
                mResolver.insert(CONTENT_URI, values );
                txt.setText(myCursor.getString(0));
            }
        };
        insert_btn.setOnClickListener(insert_lis);
        //修改
        OnClickListener update_lis = new OnClickListener(){
            public void onClick(View v) {
                ContentValues values = new ContentValues();
                values.put("mycolumn", edit.getText().toString());
                String[] args = new String[] { txt.getText().toString() };
                mResolver.update(CONTENT_URI, values, "mycolumn=?", args);
                txt.setText(myCursor.getString(0));
            }
        };
        update_btn.setOnClickListener(update_lis);
       
        //刪除
        OnClickListener delete_lis = new OnClickListener(){
            public void onClick(View v) {
                String[] args = new String[] { txt.getText().toString()};
                mResolver.delete(CONTENT_URI, "mycolumn=?", args);
                txt.setText(myCursor.getString(0));               
            }
        };
        delete_btn.setOnClickListener(delete_lis);
       
        //上一筆
        OnClickListener previous_lis = new OnClickListener(){
            public void onClick(View v) {
                myCursor.moveToPrevious();
                txt.setText(myCursor.getString(0));
            }
        };
        previous_btn.setOnClickListener(previous_lis);
       
        //下一筆
        OnClickListener next_lis = new OnClickListener(){
            public void onClick(View v) {
                myCursor.moveToNext();
                txt.setText(myCursor.getString(0));
            }
        };
        next_btn.setOnClickListener(next_lis);
       
    }
   
}

程式畫面如下: