本模塊共有四篇文章,參考郭神的《第一行代碼》,對Content Provider的學習做一個詳細的筆記,大家可以一起交流一下:
- 跨程序共享數據——Content Provider 之 運行時權限解析以及申請的實現(可完美解決java.lang.SecurityException:Permission Denial 問題)
- 跨程序共享數據——Content Provider 之 ContentResolver基本用法 & 一個讀取系統(tǒng)聯系人的Demo
- 跨程序共享數據——Content Provider 之 創(chuàng)建自己的內容提供器
- Content Provider 之 最終彈 實戰(zhàn)體驗跨程序數據共享(結合SQLiteDemo)(即本文)
簡單起見,我們還是在之前的DatabaseTest項目(點擊前往碼云地址)的基礎上繼續(xù)開發(fā)。
需求是:通過內容提供器來給它加人外部訪問接口。
程序設計的步驟:
1.在A程序中注冊內容提供器,寫好接口處理方法;
具體的,全局變量:定義自定義代碼常量,定義authority常量,聲明uriMatcher和DatabaseHelper對象;
1.1 內容提供器中增刪改查的程序設計步驟為:
1.1.1 調用實例化DatabaseHelper的get方法獲得SQLiteDatabase實例化對象;
(get方法即getWritableDatabase或者getReadableDatabase)
1.1.2 接著,query需定義一個Cursor對象(cursor)用于接收返回結果;
insert需定義一個Uri對象(urireturn)用于接收insert新增的數據行的uri
update需定義一個int對象(updatedRows)用于接收受影響的行數;
delete需定義一個int對象(deletedRows)用于接收受影響的行數(被刪除的行數);
1.1.3 使用switch語句進行對uri的判斷及判斷結果的處理;
2.在需要訪問A程序的內容提供器的程序中,構建對應的Uri,通過getContentResolver調用增刪改查即可;
下面開始詳細解析:
打開DatabaseTest項目(點擊前往碼云地址),首先將MyDatabaseHelper中使用Toast彈出創(chuàng)建數據庫成功的提示去除掉,因為跨程序訪問時我們不能直接使用Toast(?。。。。?。然后創(chuàng)建一個內容提供器,右擊com.example.databasetest包—New—Other—ContentProvider,

會彈出如圖所示:

這里我們將內容提供器命名為DatabaseProvider,
authority指定為com.example.databasetest.provider,
Exported屬性表示是否允許外部程序訪問我們的內容提供器,
Enabled屬性表示是否啟用這個內容提供器。
將兩個屬性都勾中,點擊Finish完成創(chuàng)建。
接著我們修改DatabaseProvider:
package com.example.databasetest;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
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); //插入后返回一個id!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);//id構造用于返回的URI!!!!!!!!!!!!!!!!!!!!!!
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;
}
}
代碼雖然很長,不過都不難理解,都是上一節(jié)學習過的內容。
首先在類的一開始,同樣是定義了4個常量,分別用于表示
訪問Book表中的所有數據;
訪問Book表中的單條數據;
訪問Category表中的所有數據;
訪問Category表中的單條數據。
然后在靜態(tài)代碼塊里對UriMatcher進行了初始化操作,將期望匹配的幾種URI格式添加了進去。接下來是onCreate()方法:
創(chuàng)建了一個MyDatabaseHelper的實例,然后返回true表示內容提供器初始化成功,這時數據庫就已經完成了創(chuàng)建或升級操作。接著是query()方法:
首先獲取到SQLiteDatabase實例,然后根據傳入的Uri參數判斷出用戶想要訪問哪張表,
再調用SQLiteDatabase的query()進行查詢,并將Cursor對象返回即可。
注意當訪問單條數據的時候有一個細節(jié):
這里調用了Uri對象的getpathSegments()方法,它會將內容URI權限之后的部分以“/“符號進行分割,并把分割后
的結果放入到一個字符串列表中,那這個列表的第0個位置存放的就是路徑,第1個位置存放的就是id了。
得到了id之后,再通過selection和selectionArgs參數進行約束,就實現了查詢單條數據的功能。
getPathSegments().get(1)的解釋參考:

然后是insert()方法:
同樣先獲取到SQLiteDatabase實例,
然后根據傳入的Uri參數判斷出用戶想要往哪張表里添加數據,
再調用SQLiteDatabase的insert()方法進行添加即可。
注意insert()方法要求返回一個能夠表示這條新增數據的URI,則這里還需調用Uri.parse()方法來將一個內容URI解析成Uri對象,當然這個內容URI是以新增數據的id結尾的。接著是update()方法:
先獲取SQLiteDatabase實例,
然后根據傳入的Uri參數判斷出用戶想要更新哪張表里的數據,
再調用SQLiteDatabase的update()方法進行更新即可,受影響的行數作為返回值返回。然后是delete()方法:
先獲取到SQLiteDatabase的實例,
然后根據傳入的Uri參數判斷出用戶想要刪除哪張表里的數據,
再調用SQLiteDatabase的delete()方法進行刪除即可,被刪除的行數作為返回值返回。最后是getType()方法,這里按照上一節(jié)中介紹的格式規(guī)則編寫即可。
至此內容提供器中的代碼便全部編寫完了。
另外!!內容提供器一定要在AndroidMamfest.xml文件中注冊才可以使用,
不過使用Androidstudio的快捷方式創(chuàng)建內容提供器的話,注冊會被自動完成。
打開AndroidManifest.xmI文件看一下:

可以看到<application>標簽內出現了一個新的標簽<provider>,我們使用它來注冊內容提供器DatabaseProvider。
其中android:name指定DatabaseProvider的類名,
android:authorities指定了DatabaseProvider的authority,
enabled和exported屬性則是根據我們剛才勾選的狀態(tài)自動生成的。
現在DatabaseTest這個項目便具備跨程序共享數據的功能了。
可以調試一下:
首先將DatabaseTest程序從模擬器中刪除掉,防止遺留數據的造成干擾。
然后運行一下項目,將DatabaseTest程序重新安裝在模擬器上。
接著關閉掉DatabaseTest這個項目,并創(chuàng)建一個新項目ProviderTest,
接著通過這個程序去訪問DatabaseTest中的數據,
先編寫布局文件:
<?xml version="1.0" encoding="utf-8"?>
<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/add_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Book" />
<Button
android:id="@+id/query_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query From Book" />
<Button
android:id="@+id/update_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Book" />
<Button
android:id="@+id/delete_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete From Book" />
</LinearLayout>
MainActivity.java:
package com.example.providertest;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
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 View.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", 55.55);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.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 View.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 View.OnClickListener() {
@Override
public void onClick(View v) {
// 刪除數據
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}
其中值得注意的是:

從以上代碼中,我們可以看到
DIR類型常量匹配的,selection, selectionArgs參數位是由調用本內容提供器的時候由調用方程序提供的,
而ITEM則不同,uri中已經包含了ID,我們可以使用getPathSegments將之get出來再使用,即ITEM類型常量匹配的,調用方程序無需提供selection, selectionArgs參數位(如下方的代碼截圖),uri中已經包含了信息,處理方法也在內容提供器中寫好了。
(這一點在調用getContentResolver().update()以及getContentResolver().delete()的時候都一樣)
下面進行代碼的簡析:
分別在這4個按鈕的點擊事件里面處理了增刪改查的邏輯:
添加數據的時候:
首先調用Uri.parse()將內容URI解析成Uri對象,
把要添加的數據存放到ContentValues對象中,
調用ContentResolver的insert()方法執(zhí)行添加操作即可。
注意insert()會返回一個Uri對象,這個對象中包含了新增數據的id,這里用getPathSegments()將這個id取出,稍后會用到它;查詢數據的時候:
調用Uri.parse()將內容URI解析成Uri對象,
調用ContentResolver的query()方法去查詢數據,
查詢的結果存放在Cursor對象中,
對Cursor進行遍歷,從中取出查詢結果,并一一打印出來;更新數據的時候:
調用Uri.parse()將內容URI解析成Uri對象,
把想要更新的數據存放到ContentValues對象中,
調用ContentResolver的update()方法執(zhí)行更新操作即可;
注意這里為了不讓Book表中的其他行受到影響,
在調用Uri.parse()方法時,給內容URI的尾部增加了一個id,而這個id正是添加數據時所返回的。
也就是說這里只更新剛剛添加的那條數據,不受影響B(tài)ook表中的其他行。刪除數據的時候,
解析一個以id結尾的內容URI,
調用ContentResolver的delete()方法執(zhí)行刪除操作就可以了,
由于我們在內容URI里指定了一個id,因此只會刪掉擁有相應id的那行數據,不會影響B(tài)ook表中的其他數據。
現在運行一下ProviderTest項目,效果圖如下:

點擊一下AddToBook按鈕,此時數據便已經添加到DatabaseTest程序的數據庫中了,
可以點擊QueryFromBook按鈕來檢查一下,打印日志如圖:

這里可以看到DatabaseTest程序中只有我們剛剛添加的一條數據,
databaseTest的SQLite數據庫是我們在點擊Add To Book的時候,試圖訪問DatabaseTest的內容提供器,由此DatabaseTest的內容提供器(DatabaseProvider)會觸發(fā)DatabaseProvider.java中的onCreate()方法,如下,
由此創(chuàng)建了數據庫,并返回True,這一點在 跨程序共享數據——Content Provider 之 創(chuàng)建自己的內容提供器中曾經有提及;
創(chuàng)建了數據庫之后,便添加了添加了一條數據,由此DatabaseTest程序中只有我們剛剛添加的那一條數據而已。
點擊一下Update Book按鈕來更新數據,再點擊一下Query From Book按鈕進行檢查,結果如圖:

最后點擊Delete From Book按鈕刪除數據,此時再點擊Query From Book按鈕就查詢不到數據了。
至此跨程序共享數據功能便成功實現了。
現在不僅是ProviderTest程序,任何一個程序都可以輕松訪問DatabaseTest中的數據,同時絲毫不用擔心隱私數據泄漏的問題。

