文章目錄
- 第一章 四大元件
-
- 第三元件 Content Provider
-
- (一)定義
- (二)作用
- (三)原理
- (四)具體使用
-
- (1)統一資源辨別符
- (2)MIME資料類型
- (3)ContentProvider類
- (4)輔助工具類
-
- 1、ContentPesolver類
- 2、ContentUris類
- 3、UriMatcher類
- 4、ContentObserver類
- (五)執行個體說明(資料源采用SQLite)
-
- 1、程序内通信
- 2、程序間通信(資料共享)
第一章 四大元件
第三元件 Content Provider
(一)定義
内容提供者
(二)作用
實作各個app應用/程序間進行資料互動&共享(跨程序通信)
ContentProvider=中間者角色(搬運工),真正存儲&操作資料的資料源為原來存儲資料的方式(資料庫(sqlite)、檔案、XML、網絡等等)
ContentProvider一般為存儲和擷取資料提供統一的接口,可以在不同的應用程式之間共享資料。
之是以使用ContentProvider,主要有以下幾個理由:
1、對底層資料庫抽象:ContentProvider提供了對底層資料存儲方式的抽象。比如下圖中,底層使用了SQLite資料庫,在用了ContentProvider封裝後,即使你把資料庫換成MongoDB,也不會對上層資料使用層代碼産生影響。
2、封裝統一資料類型:Android架構中的一些類需要ContentProvider類型資料。如果你想讓你的資料可以使用在如SyncAdapter, Loader, CursorAdapter等類上,那麼你就需要為你的資料做一層ContentProvider封裝。
3、用安全的方式封裝:是ContentProvider為應用間的資料互動提供了一個安全的環境。它準許你把自己的應用資料根據需求開放給其他應用進行增、删、改、查,而不用擔心直接開放資料庫權限而帶來的安全問題。
(三)原理
Android中的Binder機制
Binder是一種Android中實作跨程序通信(IPC)的方式
(四)具體使用
(1)統一資源辨別符
(1)定義:Uniform Resource Identifier,即統一資源辨別符
(2)作用:唯一辨別 ContentProvider & 其中的資料:外界程序通過 URI 找到對應的ContentProvider & 其中的資料,再進行資料操作
(3)具體使用:URI分為 系統預置 & 自定義,分别對應系統内置的資料(如通訊錄、日程表等等)和自定義資料庫
// 設定URI
Uri uri = Uri.parse("content://com.carson.provider/User/1")
// 上述URI指向的資源是:名為 `com.carson.provider`的`ContentProvider` 中表名 為`User` 中的 `id`為1的資料
// 特别注意:URI模式存在比對通配符* & #
// *:比對任意長度的任何有效字元的字元串 #:比對任意長度的數字字元的字元串
// 以下的URI 表示 比對provider的任何内容
content://com.example.app.provider/*
// 以下的URI 表示 比對provider中的table表的所有行
content://com.example.app.provider/table/#
(2)MIME資料類型
(1)作用:ContentProvider根據 URI 傳回MIME類型
(2)組成=父類型+子類型(父類型為固定類型,用于區分單條/多條記錄,子類型可自定義)
形式1:單條記錄:vnd.android.cursor.item/自定義
形式2:多條記錄:vnd.android.cursor.dir/自定義
(3)執行個體
// 單個記錄的MIME類型
vnd.android.cursor.item/vnd.yourcompanyname.contenttype
// 若一個Uri如下:content://com.example.transportationprovider/trains/122
// 則ContentProvider會通過ContentProvider.geType(url)傳回以下MIME類型
vnd.android.cursor.item/vnd.example.rail
// 多個記錄的MIME類型
vnd.android.cursor.dir/vnd.yourcompanyname.contenttype
// 若一個Uri如下:content://com.example.transportationprovider/trains
// 則ContentProvider會通過ContentProvider.geType(url)傳回以下MIME類型
vnd.android.cursor.dir/vnd.example.rail
(3)ContentProvider類
(1)資料組織方法
ContentProvider主要以表格形式組織資料(表、記錄、字段)
(2)主要使用方法
1、添加資料
public Uri insert(Uri uri, ContentValues values)// 外部程序向 ContentProvider 中添加資料
2、删除資料
public int delete(Uri uri, String selection, String[] selectionArgs)// 外部程序 删除 ContentProvider 中的資料
3、查詢資料
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)// 外部程序更新 ContentProvider 中的資料
4、修改資料
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) // 外部應用 擷取 ContentProvider 中的資料
- 上述4個方法由外部程序回調,并運作在ContentProvider程序的Binder線程池中(不是主線程)
-
存在多線程并發通路,需要實作線程同步
a. 若ContentProvider的資料存儲方式是使用SQLite & 一個,則不需要,因為SQLite内部實作好了線程同步,若是多個SQLite則需要,因為SQL對象之間無法進行線程同步
b. 若ContentProvider的資料存儲方式是記憶體,則需要自己實作線程同步
5、其他方法
public boolean onCreate()
// ContentProvider建立後 或 打開系統後其它程序第一次通路該ContentProvider時 由系統進行調用
// 注:運作在ContentProvider程序的主線程,故不能做耗時操作
public String getType(Uri uri)
// 得到資料類型,即傳回目前 Url 所代表資料的MIME類型
(3)Android為常見的資料(如通訊錄、日程表等)提供了内置了預設的ContentProvider,也可根據需求自定義ContentProvider,上述6個方法必須重寫。ContentProvider類不會直接與外部程序互動,而是通過ContentResolver 類。
(4)輔助工具類
1、ContentPesolver類
1、作用:統一管理不同 ContentProvider間的操作
- 即通過 URI 即可操作 不同的ContentProvider 中的資料
- 外部程序通過 ContentResolver類 進而與ContentProvider類進行互動
2、一款應用要使用多個ContentProvider(手機中可能安裝很多含有Provider應用,比如聯系人應用、月曆應用、字典應用等等),故用一個ContentResolver對所有ContentProvider統一管理能降低操作成本,實作難度小。
ContentResolver通過URL來差別不同的ContentProvider
3、使用方法
3.1)具體使用:ContentResolver提供了與ContentProvider類相同名字&作用的4個方法
// 外部程序向 ContentProvider 中添加資料
public Uri insert(Uri uri, ContentValues values)
// 外部程序 删除 ContentProvider 中的資料
public int delete(Uri uri, String selection, String[] selectionArgs)
// 外部程序更新 ContentProvider 中的資料
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
// 外部應用 擷取 ContentProvider 中的資料
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
3.2)執行個體說明
// 使用ContentResolver前,需要先擷取ContentResolver
// 可通過在所有繼承Context的類中 通過調用getContentResolver()來獲得ContentResolver
ContentResolver resolver = getContentResolver();
// 設定ContentProvider的URI
Uri uri = Uri.parse("content://cn.scu.myprovider/user");
// 根據URI 操作 ContentProvider中的資料
// 此處是擷取ContentProvider中 user表的所有記錄
Cursor cursor = resolver.query(uri, null, null, null, "userid desc");
2、ContentUris類
2.1)作用:操作URI
2.2)具體使用:withAppendedId() &parseId()
//withAppendedId()作用:向URI追加一個id
Uri uri = Uri.parse("content://cn.scu.myprovider/user")
Uri resultUri = ContentUris.withAppendedId(uri, 7);
// 最終生成後的Uri為:content://cn.scu.myprovider/user/7
// parseId()作用:從URL中擷取ID
Uri uri = Uri.parse("content://cn.scu.myprovider/user/7")
long personid = ContentUris.parseId(uri);
//擷取的結果為:7
3、UriMatcher類
3.1)作用:
1、在ContentProvider中注冊URI(2)根據URI比對ContentProvider對應的資料表
3.2)具體使用:
// 步驟1:初始化UriMatcher對象
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
//常量UriMatcher.NO_MATCH = 不比對任何路徑的傳回碼
// 即初始化時不比對任何東西
// 步驟2:在ContentProvider 中注冊URI(addURI())
int URI_CODE_a = 1;
int URI_CODE_b = 2;
matcher.addURI("cn.scu.myprovider", "user1", URI_CODE_a);
matcher.addURI("cn.scu.myprovider", "user2", URI_CODE_b);
// 若URI資源路徑 = content://cn.scu.myprovider/user1 ,則傳回注冊碼URI_CODE_a
// 若URI資源路徑 = content://cn.scu.myprovider/user2 ,則傳回注冊碼URI_CODE_b
// 步驟3:根據URI 比對 URI_CODE,進而比對ContentProvider中相應的資源(match())
@Override
public String getType(Uri uri) {
Uri uri = Uri.parse(" content://cn.scu.myprovider/user1");
switch(matcher.match(uri)){
// 根據URI比對的傳回碼是URI_CODE_a
// 即matcher.match(uri) == URI_CODE_a
case URI_CODE_a:
return tableNameUser1;
// 如果根據URI比對的傳回碼是URI_CODE_a,則傳回ContentProvider中的名為tableNameUser1的表
case URI_CODE_b:
return tableNameUser2;
// 如果根據URI比對的傳回碼是URI_CODE_b,則傳回ContentProvider中的名為tableNameUser2的表
}
}
4、ContentObserver類
4.1)定義:内容觀察者
4.2)作用:觀察 Uri引起 ContentProvider 中的資料變化 & 通知外界(即通路該資料通路者):當ContentProvider 中的資料發生變化(增、删 & 改)時,就會觸發該 ContentObserver類通知資料變化
适用場景:需要頻繁檢測的資料庫或者某個資料是否發生改變,如果使用線程去操作,很不經濟而且很耗時 。
4.3)具體使用:
// 步驟1:注冊内容觀察者ContentObserver
getContentResolver().registerContentObserver(uri);
// 通過ContentResolver類進行注冊,并指定需要觀察的URI
// 步驟2:當該URI的ContentProvider資料發生變化時,通知外界(即通路該ContentProvider資料的通路者)
public class UserContentProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
db.insert("user", "userid", values);
getContext().getContentResolver().notifyChange(uri, null);
// 通知通路者
}
}
// 步驟3:解除觀察者
getContentResolver().unregisterContentObserver(uri);
// 同樣需要通過ContentResolver類進行解除
4.4)執行個體:
觀察系統的短資訊資料發生了變化。當監聽到短信資料發生變化時,查詢所有已發送的短信并且顯示出來。
(1)建立觀察系統裡短消息的資料庫變化的ContentObserver派生類SMSContentObserver.java
//用來觀察系統裡短消息的資料庫變化 ”表“内容觀察者,隻要資訊資料庫發生變化,都會觸發該ContentObserver 派生類
public class SMSContentObserver extends ContentObserver {
private static String TAG = "SMSContentObserver";
private int MSG_OUTBOXCONTENT = 2 ;
private Context mContext ;
private Handler mHandler ; //用Handler更新UI線程
public SMSContentObserver(Context context,Handler handler) {
super(handler);
mContext = context ;
mHandler = handler ;
}
//當所監聽的Uri中資料發生變化回收,就會回調該方法。
@Override
public void onChange(boolean selfChange){
Log.i(TAG, "the sms table has changed");
//查詢發件箱裡的内容
Uri outSMSUri = Uri.parse("content://sms/sent") ;
Cursor c = mContext.getContentResolver().query(outSMSUri, null, null, null,"date desc");
if(c != null){
Log.i(TAG, "the number of send is"+c.getCount()) ;
StringBuilder sb = new StringBuilder() ;
//循環周遊
while(c.moveToNext()){
// sb.append("發件人手機号碼: "+c.getInt(c.getColumnIndex("address")))
// .append("資訊内容: "+c.getInt(c.getColumnIndex("body")))
// .append("是否檢視: "+c.getInt(c.getColumnIndex("read")))
// .append("發送時間: "+c.getInt(c.getColumnIndex("date")))
// .append("\n");
sb.append("發件人手機号碼: "+c.getInt(c.getColumnIndex("address")))
.append("資訊内容: "+c.getString(c.getColumnIndex("body")))
.append("\n");
}
c.close();
mHandler.obtainMessage(MSG_OUTBOXCONTENT, sb.toString()).sendToTarget();
}
}
}
(2)主流程MainActivity注冊内容觀察者,并監聽資料變化
public class MainActivity extends Activity {
private TextView tvAirplane;
private EditText etSmsoutbox;
// Message 類型值
private static final int MSG_OUTBOXCONTENT = 1;
private SMSContentObserver smsContentObserver;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
tvAirplane = (TextView) findViewById(R.id.tvAirplane);
etSmsoutbox = (EditText) findViewById(R.id.smsoutboxContent);
// 建立ContentObserver對象
smsContentObserver = new SMSContentObserver(this, mHandler);
//步驟1:注冊内容觀察者
registerContentObservers() ;
}
private void registerContentObservers() {
//指定監聽Uri并注冊監聽該Uri,該Uri的contentProvider中資料變化會執行notifychange()通知Observer資料變化
Uri smsUri = Uri.parse("content://sms");
getContentResolver().registerContentObserver(smsUri, true,smsContentObserver);
}
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
System.out.println("---mHanlder----");
switch (msg.what) {
case MSG_OUTBOXCONTENT:
String outbox = (String) msg.obj;
//顯示短信資訊
etSmsoutbox.setText(outbox);
break;
default:
break;
}
}
};
}
(五)執行個體說明(資料源采用SQLite)
1、程序内通信
步驟說明:
- 建立資料庫類
- 自定義 ContentProvider 類
- 注冊 建立的 ContentProvider類
- 程序内通路 ContentProvider的資料
(1)建立資料庫類(SQLite資料庫封裝類)
public class DBHelper extends SQLiteOpenHelper {
// 資料庫名
private static final String DATABASE_NAME = "finch.db";
// 表名
public static final String USER_TABLE_NAME = "user";
public static final String JOB_TABLE_NAME = "job";
private static final int DATABASE_VERSION = 1;
//資料庫版本号
public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// 建立兩個表格:使用者表 和職業表
db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT)");
db.execSQL("CREATE TABLE IF NOT EXISTS " + JOB_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " job TEXT)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
(2)自定義 ContentProvider 類
public class MyProvider extends ContentProvider {
private Context mContext;
DBHelper mDbHelper = null;
SQLiteDatabase db = null;
public static final String AUTOHORITY = "cn.scu.myprovider";
// 設定ContentProvider的授權資訊(唯一辨別),一般為包名
public static final int User_Code = 1;
public static final int Job_Code = 2;
// UriMatcher類使用:在ContentProvider 中注冊URI
private static final UriMatcher mMatcher;
static{
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 初始化
mMatcher.addURI(AUTOHORITY,"user", User_Code);
mMatcher.addURI(AUTOHORITY, "job", Job_Code);
// 若URI資源路徑 = content://cn.scu.myprovider/user ,則傳回注冊碼User_Code
// 若URI資源路徑 = content://cn.scu.myprovider/job ,則傳回注冊碼Job_Code
}
// 重寫ContentProvider的6個方法
/**
* 初始化ContentProvider
*/
@Override
public boolean onCreate() {
mContext = getContext();
// 在ContentProvider建立時對資料庫進行初始化
// 運作在主線程,故不能做耗時操作,此處僅作展示
mDbHelper = new DBHelper(getContext());
db = mDbHelper.getWritableDatabase();
// 初始化兩個表的資料(先清空兩個表,再各加入一個記錄)
db.execSQL("delete from user");
db.execSQL("insert into user values(1,'Carson');");
db.execSQL("insert into user values(2,'Kobe');");
db.execSQL("delete from job");
db.execSQL("insert into job values(1,'Android');");
db.execSQL("insert into job values(2,'iOS');");
return true;
}
/**
* 添加資料
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
// 根據URI比對 URI_CODE,進而比對ContentProvider中相應的表名
// 該方法在最下面
String table = getTableName(uri);
// 向該表添加資料
db.insert(table, null, values);
// 當該URI的ContentProvider資料發生變化時,通知外界(即通路該ContentProvider資料的通路者)
mContext.getContentResolver().notifyChange(uri, null);
// // 通過ContentUris類從URL中擷取ID
// long personid = ContentUris.parseId(uri);
// System.out.println(personid);
return uri;
}
/**
* 查詢資料
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 根據URI比對 URI_CODE,進而比對ContentProvider中相應的表名
// 該方法在最下面
String table = getTableName(uri);
// // 通過ContentUris類從URL中擷取ID
// long personid = ContentUris.parseId(uri);
// System.out.println(personid);
// 查詢資料
return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,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比對 URI_CODE,進而比對ContentProvider中相應的表名
*/
private String getTableName(Uri uri){
String tableName = null;
switch (mMatcher.match(uri)) {
case User_Code:
tableName = DBHelper.USER_TABLE_NAME;
break;
case Job_Code:
tableName = DBHelper.JOB_TABLE_NAME;
break;
}
return tableName;
}
}
(3)注冊 建立的 ContentProvider類
<provider android:name="MyProvider"
android:authorities="cn.scu.myprovider"
/>
(4)程序内通路 ContentProvider的資料
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 對User表進行操作
// 設定URI
Uri uri_user = Uri.parse("content://cn.scu.myprovider/user");
// 插入表中資料
ContentValues values = new ContentValues();
values.put("_id", 3);
values.put("name", "Iverson");
// 擷取ContentResolver
ContentResolver resolver = getContentResolver();
// 通過ContentResolver 根據URI 向ContentProvider中插入資料
resolver.insert(uri_user,values);
// 通過ContentResolver 向ContentProvider中查詢資料
Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
while (cursor.moveToNext()){
System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
// 将表中資料全部輸出
}
cursor.close();
// 關閉遊标
//對job表進行操作
// 和上述類似,隻是URI需要更改,進而比對不同的URI CODE,進而找到不同的資料資源
Uri uri_job = Uri.parse("content://cn.scu.myprovider/job");
// 插入表中資料
ContentValues values2 = new ContentValues();
values2.put("_id", 3);
values2.put("job", "NBA Player");
// 擷取ContentResolver
ContentResolver resolver2 = getContentResolver();
// 通過ContentResolver 根據URI 向ContentProvider中插入資料
resolver2.insert(uri_job,values2);
// 通過ContentResolver 向ContentProvider中查詢資料
Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
while (cursor2.moveToNext()){
System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
// 将表中資料全部輸出
}
cursor2.close();
// 關閉遊标
}
}
2、程序間通信(資料共享)
(1)程序1:
1、建立資料庫類
2、自定義 ContentProvider 類
3、注冊 建立的 ContentProvider 類
<provider
android:name="MyProvider"
android:authorities="scut.carson_ho.myprovider"
// 聲明外界程序可通路該Provider的權限(讀 & 寫)
android:permission="scut.carson_ho.PROVIDER"
// 權限可細分為讀 & 寫的權限
// 外界需要聲明同樣的讀 & 寫的權限才可進行相應操作,否則會報錯
// android:readPermisson = "scut.carson_ho.Read"
// android:writePermisson = "scut.carson_ho.Write"
// 設定此provider是否可以被其他程序使用
android:exported="true"
/>
// 聲明本應用 可允許通信的權限
<permission android:name="scut.carson_ho.Read" android:protectionLevel="normal"/>
// 細分讀 & 寫權限如下,但本Demo直接采用全權限
// <permission android:name="scut.carson_ho.Write" android:protectionLevel="normal"/>
// <permission android:name="scut.carson_ho.PROVIDER" android:protectionLevel="normal"/>
(2)程序2:
(1)聲明可通路的權限
(2)通路ContentProvider類
1、聲明可通路的權限
// 聲明本應用可允許通信的權限(全權限)
<uses-permission android:name="scut.carson_ho.PROVIDER"/>
// 細分讀 & 寫權限如下,但本Demo直接采用全權限
// <uses-permission android:name="scut.carson_ho.Read"/>
// <uses-permission android:name="scut.carson_ho.Write"/>
// 注:聲明的權限必須與程序1中設定的權限對應
2、通路ContentProvider 類
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* 對user表進行操作
*/
// 設定URI
Uri uri_user = Uri.parse("content://scut.carson_ho.myprovider/user");
// 插入表中資料
ContentValues values = new ContentValues();
values.put("_id", 4);
values.put("name", "Jordan");
// 擷取ContentResolver
ContentResolver resolver = getContentResolver();
// 通過ContentResolver 根據URI 向ContentProvider中插入資料
resolver.insert(uri_user,values);
// 通過ContentResolver 向ContentProvider中查詢資料
Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
while (cursor.moveToNext()){
System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
// 将表中資料全部輸出
}
cursor.close();
// 關閉遊标
/**
* 對job表進行操作
*/
// 和上述類似,隻是URI需要更改,進而比對不同的URI CODE,進而找到不同的資料資源
Uri uri_job = Uri.parse("content://scut.carson_ho.myprovider/job");
// 插入表中資料
ContentValues values2 = new ContentValues();
values2.put("_id", 4);
values2.put("job", "NBA Player");
// 擷取ContentResolver
ContentResolver resolver2 = getContentResolver();
// 通過ContentResolver 根據URI 向ContentProvider中插入資料
resolver2.insert(uri_job,values2);
// 通過ContentResolver 向ContentProvider中查詢資料
Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
while (cursor2.moveToNext()){
System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
// 将表中資料全部輸出
}
cursor2.close();
// 關閉遊标
}
}
可見ContentProvider在程序間的通路需要聲明權限
(1)程序1聲明外界程序可通路該Provider的權限
<provider
android:name="MyProvider"
android:authorities="scut.carson_ho.myprovider"
// 聲明外界程序可通路該Provider的權限(讀 & 寫)
android:permission="scut.carson_ho.PROVIDER"
// 權限可細分為讀 & 寫的權限
// 外界需要聲明同樣的讀 & 寫的權限才可進行相應操作,否則會報錯
// android:readPermisson = "scut.carson_ho.Read"
// android:writePermisson = "scut.carson_ho.Write"
// 設定此provider是否可以被其他程序使用
android:exported="true"
/>
// 聲明本應用 可允許通信的權限
<permission android:name="scut.carson_ho.Read" android:protectionLevel="normal"/>
// 細分讀 & 寫權限如下,但本Demo直接采用全權限
// <permission android:name="scut.carson_ho.Write" android:protectionLevel="normal"/>
// <permission android:name="scut.carson_ho.PROVIDER" android:protectionLevel="normal"/>
(2)程序2聲明可通路的權限
// 聲明本應用可允許通信的權限(全權限)
<uses-permission android:name="scut.carson_ho.PROVIDER"/>
// 細分讀 & 寫權限如下,但本Demo直接采用全權限
// <uses-permission android:name="scut.carson_ho.Read"/>
// <uses-permission android:name="scut.carson_ho.Write"/>
// 注:聲明的權限必須與程序1中設定的權限對應