Android ContentProvider 實現多個應用程式共享資料

這篇文章探討的是如何在單個應用程式或者多個應用程式間做出安全的共享資料功能,此篇會運用到 ContentProvider + SQLite ,ContentProvider為應用間的數據交互提供了一個安全的環境。它準許你把自己的應用數據根據需求開放給其它應用進行新增(insert)、刪除(delete)、修改(update)、查詢(query),而不用擔心直接開放數據庫權限而帶來的安全問題。



在我還沒學習ContentProvider之前,我都是使用SharedPreferences進行資料儲存,當在儲存敏感資料的時候,再進行加密、解密的工作。SharedPreference文檔保存在App私有目錄中,可以在root手機或者調試模式run-as進入查看。說起來也不怎麼安全,並且也考慮到SharedPreference的工作效率,可以去參考這篇文章

ContentProvider是什麼?

ContentProvider是Android提供給上層的一個組件,主要用於實現數據訪問的統一管理和數據共享。這裡的數據管理是通過定義統一的訪問接口來完成,如增刪改查。同時,它採用了類似InternetURL機制,將數據以URI的形式來標識,這樣其他App就可以採用一套標準的URI規範來訪問同一處數據,而不用關心具體的實現細節。

URI :統一資源標識,格式如下:
content:// 是標準前綴
Authority:表示授權信息,是URI的標識,用於唯一標識這個ContentProvider,外部調用者可以根據這個標識來找到它。
Path:路徑名,即數據庫中的表名,用以區分ContentProvider中不同的數據表;
Id:Id號,用以區別表中的不同數據;


為什麼要使用ContentResolver ,又是什麼?

  1. 統一管理不同 "ContentProvider" 間的操作
  2. 通過URI即可操作不同的ContentProvider中的數據
  3. 外部應用程式通過ContentResolver類從而與ContentProvider類進行共享資料
有些人可能會疑惑,為什麼我們不直接訪問Provider,而是又在上面加了一層ContentResolver來進行對其的操作,這樣豈不是更複雜了嗎?其實不然,大家要知道一台手機中可不是只有一個Provider內容,它可能安裝了很多含有Provider的應用,比如聯繫人應用,日曆應用,字典應用等等。有如此多的Provider,如果你開發一款應用要使用其中多個,如果讓你去了解每個ContentProvider的不同實現,豈不是要頭都大了。所以Android為我們提供了ContentResolver來統一管理與不同ContentProvider間的操作。

以下會使用到兩個project,名字亂取分別是projectAAA、projectBBB
直接貼code:
首先projectAAA、projectBBB都創建一個ContractMemberBook.java
public class ContractMemberBook {

    public static final String AUTHORITY = "com.yang.mycontentprovider";
    public static final String PATH  = "/memberbook";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + PATH);

    public static final String CONTENT_PHONEBOOK_LIST = "vnd.android.cursor.dir/vnd.com.yang.mycontentprovider";
    public static final String CONTENT_PHONEBOOK_ITEM = "vnd.android.cursor.item/vnd.com.yang.mycontentprovider";

    // 資料庫名稱
    public static final String DATABASE_NAME = "memberbook";
    // 資料庫版本號碼
    public static final int DATABASE_VERSION = 1;

    public static class Member implements BaseColumns {

        private Member(){}

        // 表名稱
        public static final String TABLE_NAME = "member";

        public static final String ID = "_id";
        public static final String NAME = "name";
        public static final String ACCKEY = "acckey";

    }
}
Authority 可以自己取名字,但是projectAAA跟projectBBB要一致

接下來projectAAA新增SqliteDatabaseManager.java
public class SqliteDatabaseManager extends SQLiteOpenHelper {


    public SqliteDatabaseManager(Context context) {
        super(context, ContractMemberBook.DATABASE_NAME, null, ContractMemberBook.DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        String CREATE_PHONEBOOK_TABLE = "CREATE TABLE " + ContractMemberBook.Member.TABLE_NAME
                + "(" + ContractMemberBook.Member.ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                + ContractMemberBook.Member.NAME + " TEXT,"
                + ContractMemberBook.Member.ACCKEY + " TEXT" + ")";

        // 創建表格
        sqLiteDatabase.execSQL(CREATE_PHONEBOOK_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + ContractMemberBook.Member.TABLE_NAME);
        onCreate(sqLiteDatabase);
    }
}

接下來projectAAA新增MemberBookProvider.java
public class MemberBookProvider extends ContentProvider {

    private SqliteDatabaseManager dbManager;

    private static final UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        mUriMatcher.addURI(ContractMemberBook.AUTHORITY, ContractMemberBook.PATH, 1);
        mUriMatcher.addURI(ContractMemberBook.AUTHORITY, ContractMemberBook.PATH + "/#", 2);
        // 若URI資源路徑 = content://com.yang.mycontentprovider/memberbook ,則返回註冊碼 1
        // 若URI資源路徑 = content://com.yang.mycontentprovider/memberbook/數字 ,則返回註冊碼 2
    }


    // 以下是ContentProvider的6个方法

    /**
     * 初始化ContentProvider
     */
    @Override
    public boolean onCreate() {
        dbManager = new SqliteDatabaseManager(getContext());
        return false;
    }


    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, String[] projections, String selection, String[] selectionArgs, String sortOrder) {
        SQLiteDatabase db = dbManager.getWritableDatabase();
        Cursor mCursor = null;

        switch (mUriMatcher.match(uri)) {
            case 1:
                mCursor = db.query(ContractMemberBook.Member.TABLE_NAME, projections, selection, selectionArgs, null, null, null);
                break;
            case 2:
                selection = ContractMemberBook.Member.ID + " = " + uri.getLastPathSegment();
                mCursor = db.query(ContractMemberBook.Member.TABLE_NAME, projections, selection, selectionArgs, null, null, null);
                break;
            default:
                Toast.makeText(getContext(), "Invalid content uri", Toast.LENGTH_LONG).show();
                throw new IllegalArgumentException("Unknown Uri: " + uri);
        }
        mCursor.setNotificationUri(getContext().getContentResolver(), uri);
        return mCursor;
    }


    @Nullable
    @Override
    public String getType(Uri uri) {
        switch (mUriMatcher.match(uri)){
            case 1:
                return ContractMemberBook.CONTENT_PHONEBOOK_LIST;
            case 2:
                return ContractMemberBook.CONTENT_PHONEBOOK_ITEM;
            default:
                throw new IllegalArgumentException("Unknown Uri: " + uri);
        }
    }


    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues contentValues) {
        SQLiteDatabase db = dbManager.getWritableDatabase();

        if (mUriMatcher.match(uri) != 1) {
            throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        long rowId = db.insert(ContractMemberBook.Member.TABLE_NAME, null, contentValues);
        if(rowId > 0) {
            Uri uriMemberbook = ContentUris.withAppendedId(ContractMemberBook.CONTENT_URI, rowId);
            getContext().getContentResolver().notifyChange(uriMemberbook, null);
            return uriMemberbook;
        }
        throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SQLiteDatabase db = dbManager.getWritableDatabase();
        int count = 0;
        switch(mUriMatcher.match(uri)) {
            case 1:
                count = db.delete(ContractMemberBook.Member.TABLE_NAME, selection, selectionArgs);
                break;
            case 2:
                String rowId = uri.getPathSegments().get(1);
                count = db.delete(ContractMemberBook.Member.TABLE_NAME, ContractMemberBook.Member.ID + " = " + rowId
                        + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ")" : ""), selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
        SQLiteDatabase db = dbManager.getWritableDatabase();
        int count = 0;
        switch (mUriMatcher.match(uri)){
            case 1:
                count = db.update(ContractMemberBook.Member.TABLE_NAME, contentValues, selection, selectionArgs);
                break;

            case 2:
                String rowId = uri.getPathSegments().get(1);
                count = db.update(ContractMemberBook.Member.TABLE_NAME, contentValues, ContractMemberBook.Member.ID + " = " + rowId +
                        (!TextUtils.isEmpty(selection) ? " AND (" + ")" : ""), selectionArgs);
                break;

            default:
                throw new IllegalArgumentException("Unknown Uri: " + uri );
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }
}

怎麼新增資料呢?

        ContentValues values = new ContentValues();
        values.put(ContractMemberBook.Member.NAME, "Devansh");
        values.put(ContractMemberBook.Member.ACCKEY, "TYATOKYOHOT1223");

        ContentValues values2 = new ContentValues();
        values2.put(ContractMemberBook.Member.NAME, "Hulk");
        values2.put(ContractMemberBook.Member.ACCKEY, "HAHAHA@#$DD");

        Uri mUri = getContentResolver().insert(ContractMemberBook.CONTENT_URI, values);
        Uri mUri2 = getContentResolver().insert(ContractMemberBook.CONTENT_URI, values2);
        if (mUri != null) {
            Toast.makeText(getApplicationContext(), "Successfully added to Content Provider", Toast.LENGTH_LONG).show();
        } else {
            Toast.makeText(getApplicationContext(), "Failed", Toast.LENGTH_LONG).show();
        }

怎麼查詢資料呢?

        Uri CONTENT_URI = Uri.parse("content://" + ContractMemberBook.AUTHORITY + ContractMemberBook.PATH);
        // 通過ContentResolver 向ContentProvider中查詢數據
        Cursor cursor = getContentResolver().query(CONTENT_URI, null, null, null, null);
        while (cursor.moveToNext()) {
            Log.e(TAG, "Member book:" + cursor.getString(0) + " " + cursor.getString(1) + " " + cursor.getString(2));
            // 將表中的數據全部輸出
        }
        cursor.close();
        // 關閉游標

最重要的地方來了

權限:

projectAAA 的 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yang.mycontentprovideraaa">

    <!--<permission-->
        <!--android:name="com.yang.permission.myprovider"-->
        <!--android:protectionLevel="normal"/>-->
    <permission
        android:name="com.yang.permission.READ_PERMISSION"
        android:protectionLevel="signature"/>
    <permission
        android:name="com.yang.permission.WRITE_PERMISSION"
        android:protectionLevel="signature"/>


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:authorities="com.yang.mycontentprovider"
            android:name=".MemberBookProvider"
            android:readPermission="com.yang.permission.READ_PERMISSION"
            android:writePermission="com.yang.permission.WRITE_PERMISSION"
            android:exported="true" />

        <!--android:permission="com.yang.permission.myprovider"-->

    </application>

</manifest>
權限說明:我這邊需要的是多個應用程式共享資料,所以別人的應用程式不可以來存取我的資料,那如何設定呢?

1.  exported 設定為 true 是指這個provider是否被其他應用程式使用
2. permission 可以合在一起設定,或者拆分為readPermissionwritePermission
3.  protectionLevel 也分為下列幾種:
  • Normal - 任何應用程式都可以申請權限,在安裝應用程式時,不會直接提示用戶,點擊全部才會顯示
  • Dangerous - 任何應用程式都可以申請權限,在安裝應用程式時,會直接提示用戶
  • Signature - 只有跟該App使用相同私鑰簽名的應用程式才可以申請該權限
  • SignatureOrSystem - 只有跟該App使用相同私鑰簽名的應用程式,或者在/system/app才可以申請該權限
而在projectBBB 的 AndroidManifest.xml
<!--<uses-permission android:name="com.yang.permission.myprovider"/>-->
    <uses-permission android:name="com.yang.permission.READ_PERMISSION"/>
    <uses-permission android:name="com.yang.permission.WRITE_PERMISSION"/>


這樣就完成了,以上為記錄,如有問題都可留言指教。

留言

這個網誌中的熱門文章

Android - 使用 adb 安装apk

Android TextView autosizing 自動調整大小

Kotlin - 實現Android中的Parcelable