本文參考文獻:《瘋狂Android講義 : 第2版 》
ContentProvider 是不同應用程序之間進行數(shù)據(jù)交換的標準 API,ContentProvider 以某種 Uri 的形式對外提供數(shù)據(jù),允許其他應用訪問或修改數(shù)據(jù);其他應用程序使用 ContentResolver 根據(jù) Uri 去訪問操作指定數(shù)據(jù)。
完整地開發(fā)一個 ContentProvider 的步驟:
- 定義自己的 ContentProvider 類,該類需要繼承 Android 提供的 ContentProvider 基類;
- 在 AndroidManifest.xml 文件中注冊這個 ContentProvider,在注冊時綁定一個 Uri。
Uri 簡介
Uri 可以分為如下三個部分:
- content:// —— 這個部分是 Android 的 ContentProvider 規(guī)定的,就像上網(wǎng)的協(xié)議默認是 http:// 一樣。暴露 ContentProvider、訪問 ContentProvider 的協(xié)議默認是 content://;
- testContentProvider.toby.person —— 這個部分就是 content:// 的 authority。系統(tǒng)就是由這個部分來找到操作哪個 ContentProvider。只要訪問哪個指定的 ContentProvider,這個部分總是固定的;
- words —— 資源(數(shù)據(jù))部分,當訪問者需要訪問不同資源時,這個部分是動態(tài)改變的。
content://testContentProvider.toby.person/word/2
此時它要訪問的資源為 word/2,這意味著訪問 word 數(shù)據(jù)中 ID 為 2 的數(shù)據(jù)。
content://testContentProvider.toby.person/word/2/word
此時它要訪問的資源為 word/2,這意味著訪問 word 數(shù)據(jù)中 ID 為 2 的數(shù)據(jù)的word字段。
ContentProvider、ContentResolver、Uri 的關系
從圖中可以看出,ContentResolver 可以實現(xiàn)”間接調(diào)用“ ContentProvider 的 CRUD 方法:
- 當 A 應用調(diào)用 ContentResolver 的 insert() 方法時,實際上相當于調(diào)用了該 Uri 對應的 ContentProvider 的 insert() 方法;
- 當 A 應用調(diào)用 ContentResolver 的 update() 方法時,實際上相當于調(diào)用了該 Uri 對應的 ContentProvider 的 update() 方法;
- 當 A 應用調(diào)用 ContentResolver 的 delete() 方法時,實際上相當于調(diào)用了該 Uri 對應的 ContentProvider 的 delete() 方法;
- 當A應用調(diào)用 ContentResolver 的 query() 方法時,實際上相當于調(diào)用了該 Uri 對應的 ContentProvider 的 query() 方法。
開發(fā) ContentProvider 子類
開發(fā) ContentProvider 只要如下兩步:
- 開發(fā)一個 ContentProvider 子類,該子類需要實現(xiàn) query()、insert()、update() 和 delete() 等方法;
- 在AndroidManifest.xml 文件中注冊該 ContentProvider,指定 android:authorities 屬性。
配置 ContentProvider
只要為 <applicaton.../> 元素添加了 <provider.../> 子元素即可配置 ContentProvider。例如如下的配置片段:
<provider
android:name=".FirstProvider"
android:authorities="com.toby.personal.testlistview.FirstProvider"
android:exported="true"/>
下面是一個簡單的使用示例,示例01:
示例01,主布局文件的內(nèi)容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/container"
android:background="@color/colorGray"
android:orientation="vertical"
>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/query"
android:onClick="query"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/insert"
android:onClick="insert"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/update"
android:onClick="update"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/delete"
android:onClick="delete"
/>
</LinearLayout>
示例01 的 FirstProvider.java 文件的內(nèi)容:
package com.toby.personal.testlistview;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
/**
* Created by toby on 2017/4/23.
*/
public class FirstProvider extends ContentProvider {
final private static String TAG = "Toby_Provider";
@Override
public boolean onCreate() {
Log.d(TAG, "========= onCreate is called =========");
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Log.d(TAG, "========= query is called selection =========> " + selection);
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
Log.d(TAG, "========= insert is called values =========> " + values);
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d(TAG, "========= delete is called selection =========> " + selection);
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d(TAG, "========= update is called selection =========> " + selection);
return 0;
}
}
我們需要在 AndroidManifest.xml 中的 application 節(jié)點下加入如下代碼:
<provider
android:authorities="com.toby.personal.testlistview.FirstProvider"
android:name=".FirstProvider"
android:exported="true"
/>
最后是主程序文件的代碼:
package com.toby.personal.testlistview;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
final private static String TAG = "Toby_Test";
ContentResolver contentResolver;
Uri uri = Uri.parse("content://com.toby.personal.testlistview.FirstProvider/");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
contentResolver = getContentResolver();
}
public void query(View source) {
Cursor c = contentResolver.query(uri, null, "query_where", null, null);
Toast.makeText(this, "遠端 ContentProvider 返回的 Cursor 為:" + c,
Toast.LENGTH_LONG).show();
}
public void insert(View source) {
ContentValues values = new ContentValues();
values.put("name", "jsdhfkjsjh");
Uri newUri = contentResolver.insert(uri, values);
Toast.makeText(this, "遠端 ContentProvider 新插入記錄的 Uri 為:" + newUri,
Toast.LENGTH_LONG).show();
}
public void update(View source) {
ContentValues values = new ContentValues();
values.put("name", "jsdhfkjsjh");
int count = contentResolver.update(uri, values, "update_where", null);
Toast.makeText(this, "遠端 ContentProvider 更新記錄數(shù)為:" + count,
Toast.LENGTH_LONG).show();
}
public void delete(View source) {
int count = contentResolver.delete(uri, "update_where", null);
Toast.makeText(this, "遠端 ContentProvider 刪除記錄數(shù)為:" + count,
Toast.LENGTH_LONG).show();
}
}
該示例運行之后,控制臺的 log 監(jiān)控效果:
配置ContentProvider時通常指定如下屬性:
- name:指定該ContentProvider的實現(xiàn)類的類名。
- authorities:指定該ContentProvider對應的Uri(相當于為該ContentProvider分配一個域名。)
- android:exported:指定該ContentProvider是否允許其他應用調(diào)用。如果將該屬性設為false,那么該ContentProvider將不允許其他應用調(diào)用。
為了確定ContentProvider實際能處理的Uri,以及確定每個方法中Uri參數(shù)所操作的數(shù)據(jù),Android系統(tǒng)提供了UriMatcher工具類,主要提供了如下兩個方法:
- void addURI(String authority ,String path ,int code):該方法用于向UriMatcher對象注冊Uri。其中authority和path組合成一個Uri,而code則代表該Uri對應的標識碼。
- int match(Uri uri):根據(jù)前面注冊的Uri來判斷指定Uri對應的標識碼。如果找不到匹配的標識碼,就會返回-1。
例如:
UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.toby.personal.testlistview.FirstProvider", "words", 1);
uriMatcher.addURI("com.toby.personal.testlistview.FirstProvider", "word/#", 2);
其中 # 為通配符,這意味著如下匹配結(jié)果:
uriMatcher.match(Uri.parse("content://com.toby.personal.testlistview.FirstProvider/words"));
// 返回標識碼 1
uriMatcher.match(Uri.parse("content://com.toby.personal.testlistview.FirstProvider/word/2"));
// 返回標識碼 2
uriMatcher.match(Uri.parse("content://com.toby.personal.testlistview.FirstProvider/word/10"));
// 返回標識碼 2
至于到底需要為 UriMatcher 注冊多少個 Uri,取決于系統(tǒng)的業(yè)務需求。
對于 content://com.toby.personal.testlistview.FirstProvider/words 這個 Uri,它的資源部分為 words,這種資源通常代表了訪問所有數(shù)據(jù)項;對于 content://com.toby.personal.testlistview.FirstProvider/word/2 這個 Uri,它的資源部分通常代表訪問指定數(shù)據(jù)項,其中最后一個數(shù)值往往代表了該數(shù)據(jù)的 ID。
除此之外,Android 還提供了一個 ContentUris 工具類,它是一個操作 Uri 字符串的工具類,提供了如下兩個工具方法:
- withAppendedId(uri , id):用于為路徑加上ID部分,例如:
Uri uri = Uri.parse("content://com.toby.personal.testlistview.FirstProvider/word");
Uri resultUri = ContentUris.withAppendedId(uri, 2);
// 生成后的 Uri 為 "content://com.toby.personal.testlistview.FirstProvider/word/2"
- parseId(uri):用于從指定Uri中解析出所包含的ID值,例如:
Uri uri = Uri.parse("content://com.toby.personal.testlistview.FirstProvider/word/2");
long wordId = ContentUris.parseId(uri); // 獲取的結(jié)果為:2
操作系統(tǒng)的 ContentProvider
Android系統(tǒng)本身提供了大量的ContentProvider,使用ContentResolver操作系統(tǒng)的ContentProvider數(shù)據(jù)的步驟也是兩步:
- 調(diào)用Context的getContentResolver()獲取ContentResolver對象;
- 根據(jù)需要調(diào)用ContentResolver的insert()、delete()、update()和query()方法操作數(shù)據(jù)。
Android系統(tǒng)用于管理聯(lián)系人的ContentProvider的幾個Uri如下:
- ContactsContract.Contacts.CONTENT_URI:管理聯(lián)系人的Uri;
- ContactsContract.CommonDataKinds.Phone.CONTENT_URI:管理聯(lián)系人的電話的Uri;
- ContactsContract.CommonDataKinds.Email.CONTENT_URI:管理聯(lián)系人的E-mail的Uri。
Android為多媒體提供的ContentProvider的Uri如下所示:
- MediaStore.Audio.Media.EXTERNAL_CONTENT_URI:存儲在外部存儲其上的音頻文件內(nèi)容的ContentProvider的Uri;
- MediaStore.Audio.Media.INTERNAL_CONTENT_URI:存儲在手機內(nèi)部存儲器上的音頻文件內(nèi)容的ContentProvider的Uri;
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI:存儲在外部存儲器上的圖片文件內(nèi)容的ContentProvider的Uri;
- MediaStore.Images.Audio.Media.INTERNAL_CONTENT_URI:存儲在手機內(nèi)部存儲器上的圖片文件內(nèi)容的ContentProvider的Uri;
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI:存儲在外部存儲器上的視頻文件內(nèi)容的ContentProvider的Uri;
- MediaStore.Video.Audio.Media.INTERNAL_CONTENT_URI:存儲在手機內(nèi)部存儲器上的視頻文件內(nèi)容的ContentProvider的Uri。
監(jiān)聽 ContentProvider 的數(shù)據(jù)改變
在之前的介紹中,只要導致了 ContentProvider 數(shù)據(jù)發(fā)生了改變,程序中就調(diào)用如下代碼:
getContext().getContentResolver(),notifyChange(uri ,null);
為了在應用程序中監(jiān)聽ContentProvider數(shù)據(jù)的改變,需要利用Android提供的ContentObserver基類。監(jiān)聽ContentProvider數(shù)據(jù)改變的監(jiān)聽器需要繼承ContentObserver類,并重寫該基類所定義的onChange(boolean selfChange)方法--當所監(jiān)聽的ContentProvider數(shù)據(jù)發(fā)生改變時,該onChange()方法將會被觸發(fā)。
為了監(jiān)聽指定ContentProvider的數(shù)據(jù)變化,需要通過ContentResolver向指定Uri注冊ContentObserver監(jiān)聽器。ContentResolver提供了如下方法來注冊監(jiān)聽器:
registerContentObserver(Uri uri , boolean notifyForDescendents , ContentObserver observer)
這個方法的三個參數(shù)分別表示:
- uri —— 該監(jiān)聽器所監(jiān)聽的ContentProvider的Uri。
- notifyForDescendents —— 如果該參數(shù)設為true,假如注冊監(jiān)聽的Uri為content://abc,nameUri為contetn://abc/xyz、content://abc/xyz/foo的數(shù)據(jù)改變時也會觸發(fā)該監(jiān)聽器;如果設為false,那么只有content://abc的數(shù)據(jù)發(fā)生改變時才會觸發(fā)該監(jiān)聽器。
- observer —— 監(jiān)聽器實例。
提供程序訪問的替代形式
提供程序訪問的三種替代形式在應用開發(fā)的過程中十分重要:
- 批量訪問:可以通過 ContentProviderOperation 類中的方法創(chuàng)建一批訪問調(diào)用,然后通過 ContentResolver.applyBatch() 執(zhí)行它們。
- 異步查詢:應該在單獨線程中執(zhí)行查詢。
- 通過 Intent 訪問數(shù)據(jù):盡管無法直接向提供程序發(fā)送 Intent,但是可以向提供程序的應用發(fā)送 Intent,后者通常具有修改提供程序數(shù)據(jù)的最佳配置。