本節(jié)例程下載地址:
WillFlowContentProvider
WillFlowProviderTest
一纱新、內(nèi)容提供器簡(jiǎn)介
內(nèi)容提供程序管理一組共享的應(yīng)用數(shù)據(jù)掰担,用于在不同的應(yīng)用程序之間實(shí)現(xiàn)數(shù)據(jù)共享的功能。它提供了一套完整的機(jī)制怒炸,允許一個(gè)程序訪問另一個(gè)程序中的數(shù)據(jù),同時(shí)還能保證被訪數(shù)據(jù)的安全性毡代。我們可以將數(shù)據(jù)存儲(chǔ)在文件系統(tǒng)阅羹、SQLite 數(shù)據(jù)庫、網(wǎng)絡(luò)上或我們的應(yīng)用可以訪問的任何其他永久性存儲(chǔ)位置教寂。 其他應(yīng)用可以通過內(nèi)容提供程序查詢數(shù)據(jù)捏鱼,甚至修改數(shù)據(jù)(如果內(nèi)容提供程序允許)。 例如酪耕,Android 系統(tǒng)可提供管理用戶聯(lián)系人信息的內(nèi)容提供程序导梆。 因此,任何具有適當(dāng)權(quán)限的應(yīng)用都可以查詢內(nèi)容提供程序的某一部分(如 ContactsContract.Data),以讀取和寫入有關(guān)特定人員的信息看尼。
目前递鹉,使用內(nèi)容提供器是 Android 實(shí)現(xiàn)跨程序共享數(shù)據(jù)的標(biāo)準(zhǔn)方式。不同于文件存儲(chǔ)和 Shared Preferences 存儲(chǔ)中的兩種全局可讀寫操作模式藏斩,內(nèi)容提供器可以選擇只對(duì)哪一部分?jǐn)?shù)據(jù)進(jìn)行共享躏结,從而保證我們程序中的隱私數(shù)據(jù)不會(huì)有泄漏的風(fēng)險(xiǎn)。當(dāng)然內(nèi)容提供程序也適用于讀取和寫入我們的應(yīng)用不共享的私有數(shù)據(jù)狰域。 例如媳拴,記事本示例應(yīng)用使用內(nèi)容提供程序來保存筆記。
內(nèi)容提供器的用法一般有兩種兆览,一種是使用現(xiàn)有的內(nèi)容提供器來讀取和操作相應(yīng)程序中的數(shù)據(jù)屈溉,另一種是創(chuàng)建自己的內(nèi)容提供器給我們程序的數(shù)據(jù)提供外部訪問接口。這和我們之前學(xué)習(xí)的廣播有點(diǎn)類似是吧抬探?那么接下來我們就逐一開始學(xué)習(xí)子巾。
二、訪問其他程序中的數(shù)據(jù)
當(dāng)一個(gè)應(yīng)用程序通過內(nèi)容提供器對(duì)其數(shù)據(jù)提供了外部訪問接口驶睦,任何其他的應(yīng)用程序就都可以對(duì)這部分?jǐn)?shù)據(jù)進(jìn)行訪問砰左。 Android 系統(tǒng)中自帶的電話簿、短信场航、媒體庫等程序都提供了類似的訪問接口缠导,這就使得第三方應(yīng)用程序可以充分地利用這部分?jǐn)?shù)據(jù)來實(shí)現(xiàn)更好的功能。下面我們就來看一看溉痢,內(nèi)容提供器到底是如何使用的僻造。
1、ContentResolver 的基本用法
對(duì)于每一個(gè)應(yīng)用程序來說孩饼,如果想要訪問內(nèi)容提供器中共享的數(shù)據(jù)髓削,就一定要借助 ContentResolve 類,我們可以通過 Context 中的 getContentResolver() 方法獲取到該類的實(shí)例镀娶。ContentResolver 中提供了一系列的方法用于對(duì)數(shù)據(jù)進(jìn)行 CRUD 操作立膛,其中 insert()方法用于添加數(shù)據(jù), update()方法用于更新數(shù)據(jù)梯码, delete()方法用于刪除數(shù)據(jù)宝泵, query()方法用于查詢數(shù)據(jù)。等我我們學(xué)習(xí)到后面轩娶,你會(huì)發(fā)現(xiàn) SQLiteDatabase 中也是使用的這幾個(gè)方法來進(jìn)行 CRUD操作的儿奶,只不過它們?cè)诜椒▍?shù)上稍微有一些區(qū)別。
不同于 SQLiteDatabase鳄抒, ContentResolver 中的增刪改查方法都是不接收表名參數(shù)的闯捎,而是使用一個(gè) Uri 參數(shù)代替椰弊,這個(gè)參數(shù)被稱為內(nèi)容 URI。內(nèi)容 URI 給內(nèi)容提供器中的數(shù)據(jù)建立了唯一標(biāo)識(shí)符瓤鼻,它主要由兩部分組成秉版,權(quán)限(authority)和路徑(path)。
- 權(quán)限是用于對(duì)不同的應(yīng)用程序做區(qū)分的娱仔,一般為了避免沖突沐飘,都會(huì)采用程序包名的方式來進(jìn)行命名。比如某個(gè)程序的包名是 com.example.app牲迫,那么該程序?qū)?yīng)的權(quán)限就可以命名為 com.example.app.provider耐朴。
- 路徑則是用于對(duì)同一應(yīng)用程序中不同的表做區(qū)分的,通常都會(huì)添加到權(quán)限的后面盹憎。比如某個(gè)程序的數(shù)據(jù)庫里存在兩張表:table1 和 table2筛峭,這時(shí)就可以將路徑分別命名為/table1和/table2,然后把權(quán)限和路徑進(jìn)行組合陪每,內(nèi)容 URI 就變成了 com.example.app.provider/table1和 com.example.app.provider/table2影晓。
不過,目前還很難辨認(rèn)出這兩個(gè)字符串就是兩個(gè)內(nèi)容URI檩禾,我們還需要在字符串的頭部加上協(xié)議聲明挂签。因此,內(nèi)容 URI 最標(biāo)準(zhǔn)的格式寫法如下:
content://com.wgh.willflowcontentprovider.provider/table1/table1
content://com.wgh.willflowcontentprovider.provider/table1/table2
如果你足夠細(xì)心的話你可以發(fā)現(xiàn):內(nèi)容 URI 可以非常清楚地表達(dá)出我們想要訪問哪個(gè)程序中哪張表里的數(shù)據(jù)盼产。 也正是因此饵婆, ContentResolver 中的增刪改查方法才都接收 Uri 對(duì)象作為參數(shù),因?yàn)槭褂帽砻脑捪到y(tǒng)將無法得知我們期望訪問的是哪個(gè)應(yīng)用程序里的表戏售。
在得到了內(nèi)容 URI 字符串之后侨核,我們還需要將它解析成 Uri 對(duì)象才可以作為參數(shù)傳入。解析的方法也相當(dāng)簡(jiǎn)單灌灾,代碼如下所示:
Uri uri = Uri.parse("content://com.wgh.willflowcontentprovider.provider/table1/table1")
只需要調(diào)用 Uri.parse()方法搓译,就可以將內(nèi)容 URI 字符串解析成 Uri 對(duì)象了。
現(xiàn)在我們就可以使用這個(gè) Uri 對(duì)象來查詢 table1 表中的數(shù)據(jù)了锋喜,代碼如下所示:
Cursor cursor = getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder);
這些參數(shù)和 SQLiteDatabase 中 query() 方法里的參數(shù)很像些己,但總體來說要簡(jiǎn)單一些,畢竟這是在訪問其他程序中的數(shù)據(jù)嘿般,沒必要構(gòu)建過于復(fù)雜的查詢語句轴总。下表對(duì)使用到的這部分參數(shù)進(jìn)行了詳細(xì)的解釋。
query()方法參數(shù) | 對(duì)應(yīng) SQL 部分 | 描述 |
---|---|---|
uri | from table_name | 指定查詢某個(gè)應(yīng)用程序下的某一張表 |
projection | select column1, column2 | 指定查詢的列名 |
selection | where column = value | 指定 where 的約束條件 |
selectionArgs | --- | 為 where 中的占位符提供具體的值 |
orderBy | order by column1, column2 | 指定查詢結(jié)果的排序方式 |
查詢完成后返回的仍然是一個(gè) Cursor 對(duì)象博个,這時(shí)我們就可以將數(shù)據(jù)從 Cursor 對(duì)象中逐個(gè)讀取出來了。讀取的思路仍然是通過移動(dòng)游標(biāo)的位置來遍歷 Cursor 的所有行功偿,然后再取出每一行中相應(yīng)列的數(shù)據(jù)盆佣,代碼如下所示:
if (cursor != null) {
while (cursor.moveToNext()) {
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
掌握了最難的查詢操作往堡,剩下的增加、修改共耍、刪除操作就更不在話下了虑灰。我們先來看看如何向 table1 表中添加一條數(shù)據(jù),代碼如下所示:
ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);
可以看到痹兜,仍然是將待添加的數(shù)據(jù)組裝到 ContentValues 中穆咐,然后調(diào)用 ContentResolver 的 insert() 方法,將 Uri 和 ContentValues 作為參數(shù)傳入即可字旭。
現(xiàn)在如果我們想要更新這條新添加的數(shù)據(jù),把 column1 的值清空,可以借助 ContentResolver 的 update() 方法實(shí)現(xiàn)巍耗,代碼如下所示:
ContentValues values = new ContentValues();
values.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[] {"text", "1"});
注意上述代碼使用了 selection 和 selectionArgs 參數(shù)來對(duì)想要更新的數(shù)據(jù)進(jìn)行約束令哟,以防止所有的行都會(huì)受影響。
最后屈暗,可以調(diào)用 ContentResolver 的 delete()方法將這條數(shù)據(jù)刪除掉拆讯,代碼如下所示:
getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });
到這里為止,我們就把 ContentResolver 中的增刪改查方法全部學(xué)完了养叛,接下來种呐,我們就利用目前所學(xué)的知識(shí),看一看如何讀取系統(tǒng)電話簿中的聯(lián)系人信息弃甥。
2爽室、讀取系統(tǒng)聯(lián)系人
由于我們之前一直使用的都是模擬器,電話簿里面并沒有聯(lián)系人存在潘飘,所以現(xiàn)在需要自己手動(dòng)添加幾個(gè)肮之,以便稍后進(jìn)行讀取。打開電話簿程序卜录,我們可以通過點(diǎn)擊 Create a new contact 按鈕來對(duì)聯(lián)系人進(jìn)行創(chuàng)建戈擒。這里就先創(chuàng)建兩個(gè)聯(lián)系人吧,分別填入他們的姓名和手機(jī)號(hào)艰毒,如圖所示:
這樣準(zhǔn)備工作就做好了筐高,首先還是來編寫一下布局文件,這里我們希望讀取出來的聯(lián)系人信息能夠在 ListView 中顯示丑瞧。
修改 activity_main.xml 中的代碼柑土,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
tools:context="com.wgh.willflowcontentprovider.MainActivity">
<ListView
android:id="@+id/contacts_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
接著修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
ListView mContactsView;
ArrayAdapter<String> mAdapter;
List<String> mContactsList = new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContactsView = (ListView) findViewById(R.id.contacts_view);
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mContactsList);
mContactsView.setAdapter(mAdapter);
readContacts();
}
private void readContacts() {
Cursor cursor = null;
try {// 查詢聯(lián)系人數(shù)據(jù)
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
while (cursor.moveToNext()) {// 獲取聯(lián)系人姓名
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));// 獲取聯(lián)系人手機(jī)號(hào)
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
mContactsList.add(displayName + "\n" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
在onCreate()方法中绊汹,我們首先獲取了 ListView 控件的實(shí)例稽屏,并給它設(shè)置好了適配器,然后就去調(diào)用 readContacts()方法西乖。下面重點(diǎn)看下 readContacts()方法狐榔,可以看到坛增,這里使用了 ContentResolver 的 query()方法來查詢系統(tǒng)的聯(lián)系人數(shù)據(jù)。接著我們對(duì) Cursor 對(duì)象進(jìn)行遍歷薄腻,將聯(lián)系人姓名和手機(jī)號(hào)這些數(shù)據(jù)逐個(gè)取出收捣,聯(lián)系人姓名這一列對(duì)應(yīng)的常量是 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,聯(lián)系人手機(jī)號(hào)這一列對(duì)應(yīng)的常量是 ContactsContract.CommonDataKinds.Phone.NUMBER庵楷。兩個(gè)數(shù)據(jù)都取出之后罢艾,將它們進(jìn)行拼接,并且中間加上換行符尽纽,然后將拼接后的數(shù)據(jù)添加到 ListView 里咐蚯。最后千萬不要忘記將 Cursor 對(duì)象關(guān)閉掉。
當(dāng)然了蜓斧,讀取系統(tǒng)聯(lián)系人也是需要聲明權(quán)限的仓蛆,因此修改 AndroidManifest.xml 中的代碼,如下所示:
<uses-permission android:name="android.permission.READ_CONTACTS" />
編譯運(yùn)行看效果:
三挎春、自定義內(nèi)容提供器
1看疙、創(chuàng)建內(nèi)容提供器的步驟
前面已經(jīng)提到過,如果想要實(shí)現(xiàn)跨程序共享數(shù)據(jù)的功能直奋,官方推薦的方式就是使用內(nèi)容提供器能庆,可以通過新建一個(gè)類去繼承 ContentProvider 的方式來創(chuàng)建一個(gè)自己的內(nèi)容提供器。ContentProvider 類中有六個(gè)抽象方法脚线,我們?cè)谑褂米宇惱^承它的時(shí)候搁胆,需要將這六個(gè)方法全部重寫。
新建 MyProvider 繼承自 ContentProvider邮绿,代碼如下所示:
/**
* Created by : WGH.
*/
public class MyProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
return 0;
}
}
在這六個(gè)方法中渠旁,相信大多數(shù)你都已經(jīng)非常熟悉了,我再來簡(jiǎn)單介紹一下吧船逮。
1. onCreate()
初始化內(nèi)容提供器的時(shí)候調(diào)用顾腊。通常會(huì)在這里完成對(duì)數(shù)據(jù)庫的創(chuàng)建和升級(jí)等操作,返回 true 表示內(nèi)容提供器初始化成功挖胃,返回 false 則表示失敗杂靶。注意,只有當(dāng)存在ContentResolver 嘗試訪問我們程序中的數(shù)據(jù)時(shí),內(nèi)容提供器才會(huì)被初始化。
2. query()
從內(nèi)容提供器中查詢數(shù)據(jù)蔚舀。使用 uri 參數(shù)來確定查詢哪張表饵沧, projection 參數(shù)用于確定查詢哪些列蚀之, selection 和 selectionArgs 參數(shù)用于約束查詢哪些行, sortOrder 參數(shù)用于對(duì)結(jié)果進(jìn)行排序捷泞, 查詢的結(jié)果存放在 Cursor 對(duì)象中返回。
3. insert()
向內(nèi)容提供器中添加一條數(shù)據(jù)寿谴。使用 uri 參數(shù)來確定要添加到的表锁右,待添加的數(shù)據(jù)保存在 values 參數(shù)中咏瑟。添加完成后,返回一個(gè)用于表示這條新記錄的 URI痪署。
4. update()
更新內(nèi)容提供器中已有的數(shù)據(jù)码泞。使用 uri 參數(shù)來確定更新哪一張表中的數(shù)據(jù),新數(shù)據(jù)保存在 values 參數(shù)中悯森, selection 和 selectionArgs 參數(shù)用于約束更新哪些行宋舷, 受影響的行數(shù)將作為返回值返回。
5. delete()
從內(nèi)容提供器中刪除數(shù)據(jù)瓢姻。使用 uri 參數(shù)來確定刪除哪一張表中的數(shù)據(jù)祝蝠, selection 和 electionArgs 參數(shù)用于約束刪除哪些行, 被刪除的行數(shù)將作為返回值返回幻碱。
6. getType()
根據(jù)傳入的內(nèi)容 URI 來返回相應(yīng)的 MIME 類型绎狭。
可以看到,幾乎每一個(gè)方法都會(huì)帶有 Uri 這個(gè)參數(shù)褥傍,這個(gè)參數(shù)也正是調(diào)用 ContentResolver 的增刪改查方法時(shí)傳遞過來的儡嘶。而現(xiàn)在,我們需要對(duì)傳入的 Uri 參數(shù)進(jìn)行解析摔桦,從中分析出調(diào)用方期望訪問的表和數(shù)據(jù)社付。
回顧一下,一個(gè)標(biāo)準(zhǔn)的內(nèi)容 URI 寫法是這樣的:
content://com.wgh.willflowcontentprovider.provider/table1
這就表示調(diào)用方期望訪問的是 com.com.wgh.willflowcontentprovider 這個(gè)應(yīng)用的 table1 表中的數(shù)據(jù)邻耕。除此之外鸥咖,我們還可以在這個(gè)內(nèi)容 URI 的后面加上一個(gè) id,如下所示:
content://com.wgh.willflowcontentprovider.provider/table1/1
這就表示調(diào)用方期望訪問的是 com.wgh.willflowcontentprovider 這個(gè)應(yīng)用的 table1 表中 id 為 1 的數(shù)據(jù)兄世。
內(nèi)容 URI 的格式主要就只有以上兩種啼辣,以路徑結(jié)尾就表示期望訪問該表中所有的數(shù)據(jù),以 id 結(jié)尾就表示期望訪問該表中擁有相應(yīng) id 的數(shù)據(jù)御滩。我們可以使用通配符的方式來分別匹配這兩種格式的內(nèi)容 URI鸥拧,規(guī)則如下:
- “*”:表示匹配任意長度的任意字符
- “#”:表示匹配任意長度的數(shù)字
所以党远,一個(gè)能夠匹配任意表的內(nèi)容 URI 格式就可以寫成:
content://com.wgh.willflowcontentprovider.provider/*
而一個(gè)能夠匹配 table1 表中任意一行數(shù)據(jù)的內(nèi)容 URI 格式就可以寫成:
content://com.wgh.willflowcontentprovider.provider/#
接著,我們?cè)俳柚?UriMatcher 這個(gè)類就可以輕松地實(shí)現(xiàn)匹配內(nèi)容 URI 的功能富弦。UriMatcher 中提供了一個(gè) addURI() 方法沟娱,這個(gè)方法接收三個(gè)參數(shù),可以分別把權(quán)限腕柜、路徑和一個(gè)自定義代碼傳進(jìn)去济似。這樣,當(dāng)調(diào)用 UriMatcher 的 match() 方法時(shí)盏缤,就可以將一個(gè) Uri 對(duì)象傳入,返回值是某個(gè)能夠匹配這個(gè) Uri 對(duì)象所對(duì)應(yīng)的自定義代碼唉铜,利用這個(gè)代碼,我們就可以判斷出調(diào)用方期望訪問的是哪張表中的數(shù)據(jù)了潭流。
修改 MyProvider 中的代碼碰声,如下所示:
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;
private static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.wgh.willflowcontentprovider.provider", "table1", TABLE1_DIR);
uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table1/#", TABLE1_ITEM);
uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table2", TABLE2_DIR);
uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table2/#", TABLE2_ITEM);
}
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
// 查詢table1表中的所有數(shù)據(jù)
break;
case TABLE1_ITEM:
// 查詢table1表中的單條數(shù)據(jù)
break;
case TABLE2_DIR:
// 查詢table2表中的所有數(shù)據(jù)
break;
case TABLE2_ITEM:
// 查詢table2表中的單條數(shù)據(jù)
break;
default:
break;
}
return null;
}
可以看到, MyProvider 中新增了四個(gè)整型常量,其中 TABLE1_DIR 表示訪問 table1 表中的所有數(shù)據(jù)郑象,TABLE1_ITEM 表示訪問 table1 表中的單條數(shù)據(jù)贡这, TABLE2_DIR 表示訪問 table2 表中的所有數(shù)據(jù), TABLE2_ITEM 表示訪問 table2 表中的單條數(shù)據(jù)厂榛。接著在靜態(tài)代碼塊里我們創(chuàng)建了 UriMatcher 的實(shí)例盖矫,并調(diào)用 addURI() 方法,將期望匹配的內(nèi)容 URI 格式傳遞進(jìn)去击奶,注意這里傳入的路徑參數(shù)是可以使用通配符的辈双。然后當(dāng) query() 方法被調(diào)用的時(shí)候,就會(huì)通過 UriMatcher 的 match() 方法對(duì)傳入的 Uri 對(duì)象進(jìn)行匹配柜砾,如果發(fā)現(xiàn) UriMatcher 中某個(gè)內(nèi)容 URI 格式成功匹配了該 Uri 對(duì)象湃望,則會(huì)返回相應(yīng)的自定義代碼,然后我們就可以判斷出調(diào)用方期望訪問的到底是什么數(shù)據(jù)了。
上述代碼只是以 query() 方法為例做了個(gè)示范证芭,其實(shí) insert()瞳浦、 update()、 delete() 這幾個(gè)方法的實(shí)現(xiàn)也是差不多的废士,它們都會(huì)攜帶 Uri 這個(gè)參數(shù)叫潦,然后同樣利用 UriMatcher 的 match() 方法判斷出調(diào)用方期望訪問的是哪張表,再對(duì)該表中的數(shù)據(jù)進(jìn)行相應(yīng)的操作就可以了官硝。
除此之外诅挑,還有一個(gè)方法你會(huì)比較陌生,即 getType() 方法泛源。它是所有的內(nèi)容提供器都必須提供的一個(gè)方法,用于獲取 Uri 對(duì)象所對(duì)應(yīng)的 MIME 類型忿危。 一個(gè)內(nèi)容 URI 所對(duì)應(yīng)的 MIME 字符串主要由三部分組分达箍, Android 對(duì)這三個(gè)部分做了如下格式規(guī)定:
- 必須以 vnd 開頭。
- 如果內(nèi)容 URI 以路徑結(jié)尾铺厨,則后接 android.cursor.dir/缎玫,如果內(nèi)容 URI 以 id 結(jié)尾,則后接 android.cursor.item/解滓。
- 最后接上 vnd.<authority>.<path>赃磨。
所以,對(duì)于 content://com.wgh.willflowcontentprovider.provider/table1 這個(gè)內(nèi)容 URI洼裤,它所對(duì)應(yīng)的 MIME 類型就可以寫成:
vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table1
對(duì)于 content://com.wgh.willflowcontentprovider.provider/table1/1 這個(gè)內(nèi)容 URI邻辉,它所對(duì)應(yīng)的 MIME 類型就可以寫成:
vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table1
現(xiàn)在我們可以繼續(xù)完善 MyProvider 中的內(nèi)容了,這次來實(shí)現(xiàn) getType()方法中的邏輯腮鞍,代碼如下所示:
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table2";
default:
break;
}
return null;
}
到這里值骇,一個(gè)完整的內(nèi)容提供器就創(chuàng)建完成了,現(xiàn)在任何一個(gè)應(yīng)用程序都可以使用 ContentResolver 來訪問我們程序中的數(shù)據(jù)移国。那么前面所提到的吱瘩,如何才能保證隱私數(shù)據(jù)不會(huì)泄漏出去呢?其實(shí)多虧了內(nèi)容提供器的良好機(jī)制迹缀,這個(gè)問題在不知不覺中已經(jīng)被解決了使碾。因?yàn)樗械?CRUD 操作都一定要匹配到相應(yīng)的內(nèi)容 URI 格式才能進(jìn)行的,而我們當(dāng)然不可能向 UriMatcher 中添加隱私數(shù)據(jù)的 URI祝懂,所以這部分?jǐn)?shù)據(jù)根本無法被外部程序訪問到票摇,安全問題也就不存在了。那么下面我們就來實(shí)戰(zhàn)一下嫂易,真正體驗(yàn)一回跨程序數(shù)據(jù)共享的功能兄朋。
2、實(shí)現(xiàn)跨程序數(shù)據(jù)共享
簡(jiǎn)單起見,我們還是在面創(chuàng)建的代碼基礎(chǔ)上繼續(xù)開發(fā)颅和,通過內(nèi)容提供器來給它加入外部訪問接口傅事。首先將 MyDatabaseHelper 中使用 Toast 彈出創(chuàng)建數(shù)據(jù)庫成功的提示去除掉,因?yàn)榭绯绦蛟L問時(shí)我們不能直接使用 Toast峡扩。然后添加一個(gè) DatabaseProvider 類蹭越,代碼如下所示:
/**
* Created by : WGH.
*/
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.wgh.willflowcontentprovider.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;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
// 查詢數(shù)據(jù)
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;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.category";
}
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
// 添加數(shù)據(jù)
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, contentValues);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category", null, contentValues);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
// 刪除數(shù)據(jù)
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 int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs) {
// 更新數(shù)據(jù)
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", contentValues, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", contentValues, "id = ?", new String[]{ bookId });
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", contentValues, selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", contentValues, "id = ?", new String[]{ categoryId });
break;
default:
break;
}
return updatedRows;
}
}
首先在類的一開始,同樣是定義了四個(gè)常量教届,分別用于表示訪問 Book 表中的所有數(shù)據(jù)响鹃、訪問 Book 表中的單條數(shù)據(jù)、訪問 Category 表中的所有數(shù)據(jù)和訪問 Category 表中的單條數(shù)據(jù)案训。然后在靜態(tài)代碼塊里對(duì) UriMatcher 進(jìn)行了初始化操作买置,將期望匹配的幾種 URI 格式添加了進(jìn)去。接下來就是每個(gè)抽象方法的具體實(shí)現(xiàn)了强霎,我們分別來看一下忿项。
(1)先來看下 onCreate() 方法
這個(gè)方法的代碼很短,就是創(chuàng)建了一個(gè) MyDatabaseHelper 的實(shí)例城舞,然后返回 true 表示內(nèi)容提供器初始化成功轩触,這時(shí)數(shù)據(jù)庫就已經(jīng)完成了創(chuàng)建或升級(jí)操作。
(2)接著看一下 query() 方法
在這個(gè)方法中先獲取到了 SQLiteDatabase 的實(shí)例家夺,然后根據(jù)傳入的 Uri 參數(shù)判斷出用戶想要訪問哪張表脱柱,再調(diào)用 SQLiteDatabase 的 query() 進(jìn)行查詢,并將 Cursor 對(duì)象返回就好了拉馋。注意當(dāng)訪問單條數(shù)據(jù)的時(shí)候有一個(gè)細(xì)節(jié)榨为,這里調(diào)用了 Uri 對(duì)象的 getPathSegments() 方法,它會(huì)將內(nèi)容 URI 權(quán)限之后的部分以“ /” 符號(hào)進(jìn)行分割煌茴,并把分割后的結(jié)果放入到一個(gè)字符串列表中柠逞,那這個(gè)列表的第 0 個(gè)位置存放的就是路徑,第 1 個(gè)位置存放的就是 id 了景馁。得到了 id 之后板壮,再通過 selection 和 selectionArgs 參數(shù)進(jìn)行約束,就實(shí)現(xiàn)了查詢單條數(shù)據(jù)的功能合住。
(3)再往后就是 insert() 方法
同樣它也是先獲取到了 SQLiteDatabase 的實(shí)例绰精,然后根據(jù)傳入的 Uri 參數(shù)判斷出用戶想要往哪張表里添加數(shù)據(jù),再調(diào)用 SQLiteDatabase 的 insert() 方法進(jìn)行添加就可以了透葛。注意 insert() 方法要求返回一個(gè)能夠表示這條新增數(shù)據(jù)的 URI笨使,所以我們還需要調(diào)用 Uri.parse() 方法來將一個(gè)內(nèi)容 URI 解析成 Uri 對(duì)象,當(dāng)然這個(gè)內(nèi)容 URI 是以新增數(shù)據(jù)的 id 結(jié)尾的僚害。
(4)接下來就是 update()方法了
這個(gè)方法也是先獲取 SQLiteDatabase 的實(shí)例硫椰,然后根據(jù)傳入的 Uri 參數(shù)判斷出用戶想要更新哪張表里的數(shù)據(jù),再調(diào)用 SQLiteDatabase 的 update() 方法進(jìn)行更新就好了,受影響的行數(shù)將作為返回值返回靶草。
(5)下面是 delete()方法
這里仍然是先獲取到 SQLiteDatabase 的實(shí)例蹄胰,然后根據(jù)傳入的 Uri 參數(shù)判斷出用戶想要?jiǎng)h除哪張表里的數(shù)據(jù),再調(diào)用 SQLiteDatabase 的 delete() 方法進(jìn)行刪除就好了奕翔,被刪除的行數(shù)將作為返回值返回裕寨。
這樣我們就將內(nèi)容提供器中的代碼全部編寫完了,不過離實(shí)現(xiàn)跨程序數(shù)據(jù)共享的功能還差了一小步派继,因?yàn)檫€需要將內(nèi)容提供器在 AndroidManifest.xml 文件中注冊(cè)才可以宾袜,如下所示:
<provider
android:name=".DatabaseProvider"
android:authorities="com.wgh.willflowcontentprovider.provider"
android:exported="true">
</provider>
這里我們使用了 <provider> 標(biāo)簽來對(duì) DatabaseProvider 這個(gè)內(nèi)容提供器進(jìn)行注冊(cè),在 android:name 屬性中指定了該類的全名驾窟,又在 android:authorities 屬性中指定了該內(nèi)容提供器的權(quán)限庆猫。
現(xiàn)在這個(gè)項(xiàng)目就已經(jīng)擁有了跨程序共享數(shù)據(jù)的功能了。首先運(yùn)行一下項(xiàng)目绅络,然后接著關(guān)閉掉這個(gè)項(xiàng)目阅悍,并創(chuàng)建一個(gè)新項(xiàng)目WillFlowProviderTest, 我們就將通過WillFlowProviderTest中的代碼程序去訪問剛剛啟動(dòng)又關(guān)閉掉的這個(gè)項(xiàng)目中的數(shù)據(jù)昨稼。
先來編寫一下布局文件 activity_main.xml :
<android.support.constraint.ConstraintLayout 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"
tools:context="com.wgh.willflowprovidertest.MainActivity">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加"
android:textColor="#05d602"
android:textSize="25dp" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查詢"
android:textColor="#07b7f2"
android:textSize="25dp" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改"
android:textColor="#f28007"
android:textSize="25dp" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="刪除"
android:textColor="#f20755"
android:textSize="25dp" />
</android.support.constraint.ConstraintLayout>
布局文件放置了四個(gè)按鈕,分別用于添加拳锚、查詢假栓、修改和刪除數(shù)據(jù)的。
然后修改 MainActivity 中的代碼霍掺,如下所示:
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button1:
// 添加數(shù)據(jù)
Uri uriAdd = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book");
ContentValues values = new ContentValues();
values.put("name", "WillFlow");
values.put("author", "WGH");
values.put("pages", 525);
values.put("price", 16.18);
Uri newUri = getContentResolver().insert(uriAdd, values);
newId = newUri.getPathSegments().get(1);
break;
case R.id.button2:
// 查詢數(shù)據(jù)
Uri uriQuery = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book");
Cursor cursor = getContentResolver().query(uriQuery, 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(TAG, "book name : " + name + ", author : " + author + ", pages : " + pages + ", price : " + price);
Toast.makeText(MainActivity.this, "book name : " + name + ", author : " + author + ", pages : " + pages + ", price : " + price, Toast.LENGTH_SHORT).show();
}
cursor.close();
}
break;
case R.id.button3:
// 更新數(shù)據(jù)
Uri uriUpdate = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book/" + newId);
ContentValues contentValues = new ContentValues();
contentValues.put("name", "A Flow of Wills");
contentValues.put("pages", 1314);
contentValues.put("price", 29.26);
getContentResolver().update(uriUpdate, contentValues, null, null);
break;
case R.id.button4:
// 刪除數(shù)據(jù)
Uri uriDelete = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book/" + newId);
getContentResolver().delete(uriDelete, null, null);
break;
}
}
我們分別在這四個(gè)按鈕的點(diǎn)擊事件里面處理了增刪改查的邏輯匾荆。
添加數(shù)據(jù)的時(shí)候,首先調(diào)用了 Uri.parse() 方法將一個(gè)內(nèi)容 URI 解析成 Uri 對(duì)象杆烁,然后把要添加的數(shù)據(jù)都存放到 ContentValues 對(duì)象中牙丽,接著調(diào)用 ContentResolver 的 insert() 方法執(zhí)行添加操作就可以了。注意 insert() 方法會(huì)返回一個(gè) Uri 對(duì)象兔魂,這個(gè)對(duì)象中包含了新增數(shù)據(jù)的 id烤芦,我們通過 getPathSegments() 方法將這個(gè) id 取出,稍后會(huì)用到它析校。
查詢數(shù)據(jù)的時(shí)候构罗,同樣是調(diào)用了 Uri.parse() 方法將一個(gè)內(nèi)容 URI 解析成 Uri 對(duì)象,然后調(diào)用 ContentResolver 的 query() 方法去查詢數(shù)據(jù)智玻,查詢的結(jié)果當(dāng)然還是存放在 Cursor 對(duì)象中的遂唧。之后對(duì) Cursor 進(jìn)行遍歷,從中取出查詢結(jié)果吊奢,并一一打印出來盖彭。
更新數(shù)據(jù)的時(shí)候,也是先將內(nèi)容 URI 解析成 Uri 對(duì)象,然后把想要更新的數(shù)據(jù)存放到 ContentValues 對(duì)象中召边,再調(diào)用 ContentResolver 的 update() 方法執(zhí)行更新操作就可以了铺呵。注意這里我們?yōu)榱瞬幌胱?Book 表中其他的行受到影響,在調(diào)用 Uri.parse() 方法時(shí)掌实,給內(nèi)容 URI 的尾部增加了一個(gè) id陪蜻,而這個(gè) id 正是添加數(shù)據(jù)時(shí)所返回的。這就表示我們只希望更新剛剛添加的那條數(shù)據(jù)贱鼻, Book 表中的其他行都不會(huì)受影響宴卖。
刪除數(shù)據(jù)的時(shí)候,也是使用同樣的方法解析了一個(gè)以 id 結(jié)尾的內(nèi)容 URI邻悬,然后調(diào)用 ContentResolver 的 delete() 方法執(zhí)行刪除操作就可以了症昏。由于我們?cè)趦?nèi)容 URI 里指定了一個(gè)id,因此只會(huì)刪掉擁有相應(yīng) id 的那行數(shù)據(jù)父丰, Book 表中的其他數(shù)據(jù)都不會(huì)受影響肝谭。
編譯運(yùn)行看效果:
由此可以看出,我們的跨程序共享數(shù)據(jù)功能已經(jīng)成功實(shí)現(xiàn)了蛾扇!現(xiàn)在不僅是 WillFlowProviderTest 程序攘烛,任何一個(gè)程序都可以輕松訪問 WillFlowContentProvider 中的數(shù)據(jù),而且我們還絲毫不用擔(dān)心隱私數(shù)據(jù)泄漏的問題镀首。
點(diǎn)此進(jìn)入:GitHub開源項(xiàng)目“愛閱”坟漱。
感謝優(yōu)秀的你跋山涉水看到了這里,歡迎關(guān)注下讓我們永遠(yuǎn)在一起更哄!