Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android四大組件之ContentProvider

Android四大組件之ContentProvider

編輯:關於Android編程

ContentProvider的一個重要的特點就是它是可以使得某些數據可以被跨進程訪問,一般我們的數據庫是不可跨進程被訪問,因為數據庫一般的數據是屬於某個應用程序的,如果其他程序可以隨意訪問其數據庫,這是很危險的,但是如果該應用程序的數據想分享給其他應用程序,那麼就可以通過建立一個ContentProvider,規定一些安全機制,屏蔽一些比較重要的數據被訪問,或是規定訪問權限,比如只可讀不可寫等,使其他應用程序在有限制的前提下訪問我們的數據。這樣做就會起到對數據進行保護同時又能使得有用的數據能被分享的作用。

內容提供器(ContentProvider)主要用於在不同的應用程序之間實現數據共享的功能,它提供了一套完整的機制,允許一個程序訪問另一個程序中的數據,同時還能保證被訪數據的安全性。

目前,使用內容提供器是Android實現跨程序共享數據的標准方式。不同於文件存儲和 SharedPreferences存儲中的兩種全局可讀寫操作模式,內容提供器可以選擇只對哪一部分數據進行共享,從而保證我們程序中的隱私數據不會有洩漏的風險。

內容提供器的用法一般有兩種,一種是使用現有的內容提供器來讀取和操作相應程序中的數據(常用到的是訪問系統的數據,常見的有通訊錄、日歷、短信、多媒體等),另一種是創建自己的內容提供器給我們程序的數據提供外部訪問接口

當一個應用程序通過內容提供器對其數據提供了外部訪問接口,任何其他的應用程序就都可以對這部分數據進行訪問。

Android提供了ContentProvider,一個程序可以通過實現一個ContentProvider的抽象接口將自己的數據完全暴露出去,而且ContentProviders是以類似數據庫中表的方式將數據暴露,也就是說ContentProvider就像一個“數據庫”。那麼外界獲取其提供的數據,也就應該與從數據庫中獲取數據的操作基本一樣,只不過是采用URI來表示外界需要訪問的“數據庫”。外部訪問通過ContentResolver去訪問並操作這些被暴露的數據。

ContentResolver的基本用法

可以通過 Context 中的 getContentResolver()方法獲取到該類的實例。 ContentResolver中提供了一系列的方法用於對數據進行 CRUD操作,其中 insert()方法用於添加數據,update()方法用於更新數據,delete()方法用於刪除數據,query()方法用於查詢數據。

不同於 SQLiteDatabase,ContentResolver中的增刪改查方法都是不接收表名參數的,而是使用一個Uri參數代替,這個參數被稱為內容 URI。內容 URI給內容提供器中的數據建立了唯一標識符,它主要由兩部分組成,權限(authority)路徑(path)。權限是用於對不同的應用程序做區分的,一般為了避免沖突,都會采用程序包名的方式來進行命名。比如某個程序的包名是 com.example.app,那麼該程序對應的權限就可以命名為 com.example.app. provider。路徑則是用於對同一應用程序中不同的表做區分的,通常都會添加到權限的後面。比如某個程序的數據庫裡存在兩張表,table1和 table2,這時就可以將路徑分別命名為/table1 和/table2,然後把權限和路徑進行組合,內容 URI就變成了 com.example.app.provider/table1 和 com.example.app.provider/table2。不過,目前還很難辨認出這兩個字符串就是兩個內容 URI,我們還需要在字符串的頭部加上協議聲明。因此,內容 URI最標准的格式寫法如下:

 

content://com.example.app.provider/table1 
content://com.example.app.provider/table2

 

在得到了內容 URI字符串之後,我們還需要將它解析成 Uri對象才可以作為參數傳入。 解析的方法也相當簡單,代碼如下所示:

Uri uri = Uri.parse("content://com.example.app.provider/table1")

只需要調用 Uri.parse()方法,就可以將內容 URI字符串解析成 Uri對象了。

\

\

\

更新操作:

\

\

 

讀取系統聯系人的例子:

public class MainActivity extends Activity {
    ListView contactsView;
    ArrayAdapter adapter;
    List contactsList = new ArrayList();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        contactsView = (ListView) findViewById(R.id.contacts_view);
        adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList);
        contactsView.setAdapter(adapter);
        readContacts();
    }

    private void readContacts() {
        Cursor cursor = null;
        try {
            // 查詢聯系人數據
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
            while (cursor.moveToNext()) {
                // 獲取聯系人姓名
                String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                // 獲取聯系人手機號
                String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                contactsList.add(displayName + "\n" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();

        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
}

為什麼沒有調用 Uri.parse()方法去解析一個內容 URI 字符串呢?這是因為 ContactsContract.CommonDataKinds.Phone類已經幫我們做好了封裝,提供了一個CONTENT_URI 常量,而這個常量就是使用 Uri.parse()方法解析出來的結果。

接著我們對 Cursor對象進行遍歷,將聯系人姓名和手機號這些數據逐個取出,聯系人姓名這一列對應的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,聯系人手機號這一列對應的常量是 ContactsContract.CommonDataKinds.Phone.NUMBER.

ContentProvider創建自己的內容提供器

那麼我們是如何自定義ContentProvider來對其他程序提供訪問我們數據的接口的呢?

一般我們如果有數據想被跨進程共享,就可以定義個ContentProvider,並在該ContentProvider定義訪問該數據的方法。

完成ContentProvider類,主要需要注意以下幾點:
(01) ContentProvider的數據一般是以"數據庫"或"網絡數據"的方式存儲的。如果是數據庫,則需要實現SQLiteOpenHelper類。通過SQLiteOpenHelper類新建/管理數據庫。
(02) ContentProvider主要是以Uri的形式方式訪問的(也可以通過Intent)。要通過UriMatcher注冊ContentProvider監聽的Uri。
(03) ContentProvider是一個抽象類。當我們需要以繼承ContentProvider的方式自定義ContentProvider時,需要實現query(), insert(), update(), delete(), getType(), onCreate()這六個函數。

通過新建一個類去繼承ContentProvider的方式來創建一個自己的內容提供器。 ContentProvider類中有六個抽象方法,我們在使用子類繼承它的時候,需要將這六個方法全部重寫:

public class MyProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

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

 

可以看到,幾乎每一個方法都會帶有Uri這個參數,這個參數也正是調用ContentResolver 的增刪改查方法時傳遞過來的。而現在,我們需要對傳入的 Uri參數進行解析,從中分析出調用方期望訪問的表和數據。回顧一下,一個標准的內容URI寫法是這樣的:

content://com.example.app.provider/table1

這就表示調用方期望訪問的是 com.example.app這個應用的 table1表中的數據。除此之外,我們還可以在這個內容 URI的後面加上一個 id,如下所示:

content://com.example.app.provider/table1/1

這就表示調用方期望訪問的是com.example.app這個應用的 table1表中id為1的數據。內容 URI的格式主要就只有以上兩種,以路徑結尾就表示期望訪問該表中所有的數據,以id結尾就表示期望訪問該表中擁有相應 id的數據。我們可以使用通配符的方式來分別匹配這兩種格式的內容 URI,規則如下。

1. *:表示匹配任意長度的任意字符

2. #:表示匹配任意長度的數字

所以,一個能夠匹配任意表的內容 URI格式就可以寫成:

content://com.example.app.provider/*

而一個能夠匹配 table1表中任意一行數據的內容 URI格式就可以寫成:

content://com.example.app.provider/table1/#

接著,我們再借助UriMatcher這個類就可以輕松地實現匹配內容URI的功能。UriMatcher中提供了一個 addURI()方法,這個方法接收三個參數,可以分別把權限路徑一個自定義代碼傳進去。說明通過addURI()就可以將URI注冊到UriMatcher中,從而實現ContentProvider對URI的監聽。這樣,當調用 UriMatcher的match()方法時,就可以將一個 Uri對象傳入,返回值是某個能夠匹配這個Uri對象所對應的自定義代碼,利用這個代碼,我們就可以判斷出調用方期望訪問的是哪張表中的數據了。修改 MyProvider中的代碼,如下所示:

\

\

 

在上面的代碼中,在靜態代碼塊裡我們創建了 UriMatcher的實例,並調用 addURI()方法,將期望匹配的內容 URI格式傳遞進去,注意這裡傳入的路徑參數是可以使用通配符的。然後當 query()方法被調用的時候,就會通過 UriMatcher的match()方法對傳入的 Uri對象進行匹配,如果發現 UriMatcher中某個內容 URI格式成功匹配了該 Uri對象,則會返回相應的自定義代碼,然後我們就可以判斷出調用方期望訪問的到底是什麼數據了。

上述代碼只是以query()方法為例做了個示范,其實insert()、update()、delete()這幾個方法的實現也是差不多的,它們都會攜帶Uri這個參數,然後同樣利用 UriMatcher的 match() 方法判斷出調用方期望訪問的是哪張表,再對該表中的數據進行相應的操作就可以了。

除此之外,還有一個方法你會比較陌生,即 getType()方法。它是所有的內容提供器都必須提供的一個方法,用於獲取 Uri對象所對應的MIME類型。一個內容 URI所對應的 MIME 字符串主要由三部分組分,Android對這三個部分做了如下格式規定。

\

\

 

實現跨程序數據共享實例:

public class DatabaseProvider extends ContentProvider {
    public static final int BOOK_DIR = 0;
    public static final int BOOK_ITEM = 1;
    public static final int CATEGORY_DIR = 2;
    public static final int CATEGORY_ITEM = 3;
    public static final String AUTHORITY = "com.example.databasetest.provider";
    private static UriMatcher uriMatcher;
    private MyDatabaseHelper dbHelper;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
    }

    @Override
    public boolean onCreate() {
        dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // 查詢數據
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                cursor = db.query("Book", projection, "id = ?", new String[]{bookId}, null, null, sortOrder);
                break;
            case CATEGORY_DIR:
                cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                cursor = db.query("Category", projection, "id = ?", new String[]{categoryId}, null, null, sortOrder);
                break;
            default:
                break;
        }
        return cursor;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // 添加數據
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Uri uriReturn = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
            case BOOK_ITEM:
                long newBookId = db.insert("Book", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
                break;
            case CATEGORY_DIR:
            case CATEGORY_ITEM:
                long newCategoryId = db.insert("Category", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
                break;
            default:
                break;
        }
        return uriReturn;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        // 更新數據
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int updatedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                updatedRows = db.update("Book", values, selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                updatedRows = db.update("Book", values, "id = ?", new String[]{bookId});
                break;
            case CATEGORY_DIR:
                updatedRows = db.update("Category", values, selection, selectionArgs);

                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                updatedRows = db.update("Category", values, "id= ?", new String[]{categoryId});
            	break;
            default:
                break;
        }

        return updatedRows;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 刪除數據
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int deletedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                deletedRows = db.delete("Book", selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Book", "id = ?", new String[]{bookId});
                break;
            case CATEGORY_DIR:
                deletedRows = db.delete("Category", selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Category", "id = ?", new String[]{categoryId});
                break;
            default:
                break;
        }
        return deletedRows;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.category";
        }
        return null;
    }
}

在 AndroidManifest.xml文件中注冊:


    ……
    
        ……
        
        
    

首先在類的一開始,同樣是定義了四個常量,分別用於表示訪問 Book表中的所有數據、訪問 Book表中的單條數據、訪問 Category 表中的所有數據和訪問 Category表中的單條數據。

然後在靜態代碼塊裡對 UriMatcher進行了初始化操作,將期望匹配的幾種 URI格式添加了進去。 接下來就是每個抽象方法的具體實現了。

先來看下 onCreate()方法,這個方法的代碼很短,就是創建了一個 MyDatabaseHelper的實例,然後返回 true表示內容提供器初始化成功,這時數據庫就已經完成了創建或升級操作。

接著看一下 query()方法,在這個方法中先獲取到了 SQLiteDatabase的實例,然後根據傳入的 Uri參數判斷出用戶想要訪問哪張表,再調用 SQLiteDatabase的 query()進行查詢,並將 Cursor對象返回就好了。注意當訪問單條數據的時候有一個細節,這裡調用了 Uri對象的 getPathSegments()方法,它會將內容 URI權限之後的部分以“/”符號進行分割,並把分割後的結果放入到一個字符串列表中,那這個列表的第 0個位置存放的就是路徑,第 1個位置存 放的就是 id了。得到了 id之後,再通過 selection和 selectionArgs參數進行約束,就實現了查詢單條數據的功能。

再往後就是insert()方法,同樣它也是先獲取到了 SQLiteDatabase的實例,然後根據傳入的 Uri參數判斷出用戶想要往哪張表裡添加數據,再調用 SQLiteDatabase的 insert()方法進行。添加就可以了。注意 insert()方法要求返回一個能夠表示這條新增數據的 URI,所以我們還需要調用 Uri.parse()方法來將一個內容 URI解析成 Uri對象,當然這個內容 URI是以新增數據 的 id結尾的。

接下來就是 update()方法了,也是先獲取 SQLiteDatabase的實例,然後根據傳入的 Uri參數判斷出用戶想要更新哪張表裡的數據,再調用 SQLiteDatabase的 update()方法進行更新就好了,受影響的行數將作為返回值返回。

下面是 delete()方法,這裡仍然是先獲取到 SQLiteDatabase的實例,然後根據傳入的 Uri參數判斷出用 戶想要刪除哪張表裡的數據,再調用 SQLiteDatabase的 delete()方法進行刪除就好了,被刪除的行數將作為返回值返回。

最後是 getType()方法,這個方法中的代碼完全是按照上一節中介紹的格式規則編寫的,相信已經沒有什麼解釋的必要了。

最後是注冊代碼,可以看到,這裡我們使用了標簽來對 DatabaseProvider這個內容提供器進行注冊,在 android:name屬性中指定了該類的全名,又在 android:authorities屬性中指定了該內容提供器的權限。現在DatabaseTest這個項目就已經擁有了跨程序共享數據的功能了。

其他程序訪問contentprovider實例:

現在DatabaseTest這個項目就已經擁有了跨程序共享數據的功能了,我們趕快來嘗試一下。首先需要將 DatabaseTest程序從模擬器中刪除掉,以防止上一章中產生的遺留數據對我們造成干擾。然後運行一下項目,將 DatabaseTest程序重新安裝在模擬器上了。接著關閉掉 DatabaseTest 這個項目,並創建一個新項目 ProviderTest,我們就將通過這個程序去訪問 DatabaseTest中的數據。

public class MainActivity extends Activity {
    private String newId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button addData = (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 添加數據
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
                ContentValues values = new ContentValues();
                values.put("name", "A Clash of Kings");
                values.put("author", "George Martin");
                values.put("pages", 1040);
                values.put("price", 22.85);
                Uri newUri = getContentResolver().insert(uri, values);
                newId = newUri.getPathSegments().get(1);
            }
        });
        Button queryData = (Button) findViewById(R.id.query_data);
        queryData.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 查詢數據
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
                Cursor cursor = getContentResolver().query(uri, null, null, null, null);
                if (cursor != null) {
                    while (cursor.moveToNext()) {
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));

                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d("MainActivity", "book name is " + name);
                        Log.d("MainActivity", "book author is " + author);
                        Log.d("MainActivity", "book pages is " + pages);
                        Log.d("MainActivity", "book price is " + price);
                    }

                    cursor.close();
                }
            }
        });
        Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 更新數據
                Uri uri = Uri.parse("content://com.example.databasetest. provider/book/" + newId);
                ContentValues values = new ContentValues();
                values.put("name", "A Storm of Swords");
                values.put("pages", 1216);
                values.put("price", 24.05);
                getContentResolver().update(uri, values, null, null);
            }
        });
        Button deleteData = (Button) findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 刪除數據
                Uri uri = Uri.parse("content://com.example.databasetest. provider/book/" + newId);
                getContentResolver().delete(uri, null, null);
            }
        });
    }
}



可以看到,我們分別在這四個按鈕的點擊事件裡面處理了增刪改查的邏輯。添加數據的時候,首先調用了 Uri.parse()方法將一個內容URI解析成Uri對象,然後把要添加的數據都存放到ContentValues對象中,接著調用 ContentResolver的 insert()方法執行添加操作就可以了。注意 insert()方法會返回一個 Uri 對象,這個對象中包含了新增數據的id,我們通過getPathSegments()方法將這個 id取出,稍後會用到它。

查詢數據的時候,同樣是調用了Uri.parse()方法將一個內容URI解析成Uri對象,然後調用 ContentResolver的 query()方法去查詢數據,查詢的結果當然還是存放在Cursor對象中的。之後對Cursor進行遍歷,從中取出查詢結果,並一一打印出來。

更新數據的時候,也是先將內容URI解析成 Uri對象,然後把想要更新的數據存放到ContentValues對象中,再調用 ContentResolver的 update()方法執行更新操作就可以了。注意這裡我們為了不想讓 Book表中其他的行受到影響,在調用 Uri.parse()方法時,給內容URI的尾部增加了一個 id,而這個 id正是添加數據時所返回的。這就表示我們只希望更新剛剛添加的那條數據,Book表中的其他行都不會受影響。

刪除數據的時候,也是使用同樣的方法解析了一個以 id 結尾的內容 URI,然後調用 ContentResolver的delete()方法執行刪除操作就可以了。由於我們在內容 URI裡指定了一個 id,因此只會刪掉擁有相應id的那行數據,Book表中的其他數據都不會受影響。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved