Content Provider 与 File Provider

尚鸿才
2023-12-01

Content Provider

Content Provider(内容提供器)主要用于在不同的应用程序之间实现数据共享的功能。它为应用程序存取数据提供统一的外部接口,它不同应用之间得以共享数据,同时还能保证被访问数据的安全性。

创建自定义 Content Provider

Android Studio 提供了快速创建 Content Provider 的方式,和 Broadcast Receiver 一样。

  1. 在对应包上右键 -> New -> Other -> Content Provider。填写 Provider 类名和 URI Authorities。在创建时 Authorities 可先乱填,创建完成后再修改。

  2. 在 AndroidManifest 中修改 Authorities。通常填入该 Provider 的完整类名,如在 privider 包下,则填入 com.amie.test.provider.CustomProvider

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.amie.test">
        <application ... >
            <provider
                android:name=".provider.UserInfoProvider"
                android:authorities="com.amie.test.provider.CustomProvider"
                android:enabled="true"
                android:exported="true" />
            <!-- ... -->
        </application>
    </manifest>
    
  3. Content Provider 是通过 URI 来访问的,可以借助 UriMatcher 匹配不同格式的 URI,同时所有的操作都要匹配到相应的 URI 才可以被执行。Content Provider 的标准 URI 格式为 content://<AUTHORITY>/<PATH>,例如 content://com.amie.test.provider.CustomProvider/tb_user

    URI 后面可以添加一个参数值,通常为 id。如 content://com.amie.test.provider.CustomProvider/tb_user/1 表示访问 tb_user 表中 id 为 1 的数据。对于这种格式的 URI 还可以使用通配符来匹配。

    • *:表示匹配任意长度的任意字符。
    • #:表示匹配任意长度的数字。
    public class UserInfoProvider extends ContentProvider {
        private static final String AUTHORITY = "com.amie.test.provider.UserInfoProvider";
        private static final int ALL_TABLE = 0;
        private static final int TB_USER_DIR = 1;
        private static final int TB_USER_ITEM = 2;
        private static final int TB_TEST_DIR = 3;
        private static final int TB_TEST_ITEM = 4;
    
        private static UriMatcher uriMatcher;
    
        static {
            uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
            // 第二个参数是希望匹配的路径
            // 第三个参数是自定义代码,作为 uriMatcher.match(uri) 方法的返回值
            uriMatcher.addURI(AUTHORITY, "tb_user", TB_USER_DIR);
            uriMatcher.addURI(AUTHORITY, "tb_user/#", TB_USER_ITEM);
            uriMatcher.addURI(AUTHORITY, "tb_test", TB_TEST_DIR);
            uriMatcher.addURI(AUTHORITY, "tb_test/#", TB_TEST_ITEM);
            // * 可以用来匹配所有表
            // uriMatcher.addURI(AUTHORITY, "*", ALL_TABLE);
        }
    
        // 以 query 查询和 insert 插入方法为例,匹配不同格式 URI 执行不同操作
        @Override
        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
            LogUtil.i("UserInfoProvider query");
            SQLiteDatabase db = databaseHelper.getReadableDatabase();
            Cursor cursor = null;
            switch (uriMatcher.match(uri)) {
                case TB_USER_DIR:
                    // 查询 tb_user 表中的所有数据
                    cursor = db.query("tb_user", null, null, null, null, null, sortOrder);
                    break;
                case TB_USER_ITEM:
                    // 查询 tb_user 表中的单条数据
                    cursor = db.query("tb_user", projection, selection, selectionArgs, null, null, sortOrder);
                    break;
                case TB_TEST_DIR:
                    // TODO 查询 tb_test 表中的所有数据
                    break;
                case TB_TEST_ITEM:
                    // TODO 查询 tb_test 表中的单条数据
                    break;
            }
            return cursor;
        }
    
        @Override
        public Uri insert(Uri uri, ContentValues values) {
            LogUtil.i("UserInfoProvider insert");
            Uri uriReturn = null; // content://com.amie.providerserver.provider.UserInfoProvider/user
            if (values.size() <= 0) { // 空数据直接返回
                return null;
            }
            // 客户端传入的 ContentValues 数据可能不规范,此处作为服务端一定要验证,避免插入不良数据
            // 创建一个新的 ContentValues 作为数据验证后最终插入的数据
            ContentValues mValues = null;
            SQLiteDatabase db = databaseHelper.getWritableDatabase();
            switch (uriMatcher.match(uri)) {
                case TB_USER_DIR:
                case TB_USER_ITEM:
                    mValues = new ContentValues();
                    // 获取所需数据并开始验证
                    Object name = values.get("name");
                    Object age = values.get("age");
                    Object gander = values.get("gander");
                    // _id 应该是自动生成的,如果传入值可能会破坏 _id 顺序
                    // 如果用户在 EditText 中没有输入任何内容,但是调用 getText().toString() 方法返回的是空字符串
                    // 空字符串不应该作为正常数据插入,必须手动处理
                    // 这里一定要使用 !"".equals(Object) 进行判断,使用 != 无效
                    if (name != null && !"".equals(name)) {
                        mValues.put("name", name.toString());
                        if (age != null && !"".equals(age)) {
                            mValues.put("age", Integer.parseInt(age.toString()));
                        }
                        if (gander != null && !"".equals(gander)) {
                            mValues.put("gander", Integer.parseInt(gander.toString()));
                        }
                    }
                    if (mValues.size() <= 0) { // 验证后数据为空直接返回
                        return null;
                    }
                    // 向 tb_user 表中插入数据,这里传入的值为 mValues
                    long newUserId = db.insert("tb_user", null,  mValues);
                    uriReturn = Uri.parse("content://" + AUTHORITIES + "/tb_user/" + newUserId);
                case TB_TEST_DIR:
                case TB_TEST_ITEM:
                    // TODO 向 tb_test 表中插入数据
                    break;
            }
            return uriReturn;
        }
        // ...
    }
    
  4. 上面的步骤完成后,这个 Content Provider 已经可以正常使用了,却还有一个 getType() 方法没有实现。事实上,这个方法是用来获取 Uri 对象所对应的 MIME 类型。Android 对 MIME 字符串进行了规定:

    • 必须以 vnd 开头。
    • 如果 Content URI 以路径结尾,则 vnd 后接 android.cursor.dir/,如果 Content URI 以 id 结尾,则 vnd 后接 android.cursor.item/
    • 最后接上 vnd.<AUTHORITIES>.<PATH>
    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
            case TB_USER_DIR:
                // "vnd.android.cursor.dir/vnd.com.amie.test.provider.UserInfoProvider.tb_user"
                return "vnd.android.cursor.dir/vnd." + AUTHORITIES + ".tb_user";
            case TB_USER_ITEM:
                // "vnd.android.cursor.item/vnd.com.amie.test.provider.UserInfoProvider.tb_user"
                return "vnd.android.cursor.item/vnd." + AUTHORITIES + ".tb_user";
            case TB_TEST_DIR:
                return "vnd.android.cursor.dir/vnd." + AUTHORITIES + ".tb_test";
            case TB_TEST_ITEM:
                return "vnd.android.cursor.item/vnd." + AUTHORITIES + ".tb_test";
        }
        return null;
    }
    

访问 Content Provider

前面说了 Content Provider 是用于在不同的应用程序之间实现数据共享的,那么访问 Content Provider 就需要在另一个应用中去实现。

Context 提供了一个 getContentResolver() 方法去获取 ContentResolver 对象,通过它就可以调用 insert() 等方法。

注意:出于安全考虑,Android 11 要求应用在 AndroidManifest 中事先说明需要访问的其他软件包或 provider,不然无法运行。 具体做法是在 AndroidManifest 添加 <queries> 标签及内容。

<!-- 出于安全考虑,Android 11 要求应用事先说明需要访问的其他软件包或 provider -->
<manifest ... >
    <!-- ... -->
    <queries>
        <!-- 指定服务端的包名 -->
        <package android:name="com.amie.providerserver" />
        <!-- 指定服务端 Provider 的 authorities -->
        <provider android:authorities="com.amie.providerserver.provider.UserInfoProvider" />
    </queries>
    <!-- ... -->
</manifest>

File Provider

Android 7.0 开启了严格模式(StrictMode),Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。也就是说无法直接将一个 File Uri 共享给另一个程序进行使用。

FileProvider 是Android 7.0 出现的新特性,它是 ContentProvider 的子类,可以通过创建一个 Content URI 并赋予临时的文件访问权限来代替 File URI 实现文件共享。简单来说就是将自身内部文件暴露给其他应用,并授予临时的文件读写权限。常见使用场景有,调用相机拍照和图片裁剪、应用升级调用系统应用安装器安装 APK。

使用步骤:

  1. 定义 FileProvider。

    <!-- authorities:一个标识,在当前系统内必须是唯一值,一般用 <package_name>.FileProvider。 -->
    <!-- 这里使用官方提供的 FileProvider,name 规定为 androidx.core.content.FileProvider。推荐自定义 FileProvider -->
    <!-- granUriPermissions:是否允许通过 URI 授予文件的临时访问权限。必须设置为 true。 -->
    <!-- exported:表示是否对外公开。FileProvider 不需要也不应该对任何应用公开,为了安全必须为 false。 -->
    <provider
        android:authorities="com.amie.cameraalbum.FileProvider"
        android:name="androidx.core.content.FileProvider"
        android:grantUriPermissions="true"
        android:exported="false" />
    
  2. 在 res 目录下创建 xml 安卓资源文件夹,在其中创建 file_paths.xml 文件,并定义可通过该 FileProvider 访问到的文件路径。

    <!-- res/xml/file_paths.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <!-- external-path 对应 Environment.getExternalStorageDirectory() 指向的目录。即 /storage/emulated/0 -->
        <external-path name="external_storage_root" path="." />
    
        <!-- files-path 对应 content.getFileDir() 获取到的目录。即 /data/data/<package_name>/files -->
        <files-path name="files-path" path="." />
    
        <!-- cache-path 对应 content.getCacheDir() 获取到的目录。即 /data/data/<package_name>/cache -->
        <cache-path name="cache-path" path="." />
    
        <!-- external-files-path 对应 ContextCompat.getExternalFilesDirs() 获取到的目录。即 /storage/emulated/0/Android/data/<package_name>/files -->
        <external-files-path name="external_file_path" path="." />
    
        <!-- external-cache-path 对应 ContextCompat.getExternalCacheDirs() 获取到的目录。即 /storage/emulated/0/Android/data/<package_name>/cache -->
        <external-cache-path name="external_cache_path" path="." />
    
        <!-- root-path 对应 DEVICE_ROOT,也就是 File DEVICE_ROOT = new File("/"),即根目录 "/",一般不需要配置,标签上也有警告 Element root-path is not allowed here。 -->
        <!--配置 root-path 可以读取到 sd 卡和一些应用分身的目录,否则微信等应用分身保存的图片,在读取时会出现非法参数异常 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg -->
        <root-path name="root-path" path="" />
    </paths>
    
  3. 为定义的 FileProvider 添加文件路径。

    <provider
        ... >
        <!-- 配置哪些路径可以通过 FileProvider 访问 -->
        <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/>
    </provider>
    
  4. 为特定文件生成 Content URI。FileProvider 提供了 getUriForFile() 方法生成 ContentURI。注意使用的文件路径必须是前面在 file_paths.xml 中定义的,否则无法通过该 URI 访问。

    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
        private Button takePhoto;
        private ImageView picture;
        private Uri imageUri;
        private File outputImage;
        private ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
                if (result.getResultCode() == RESULT_OK) {
                    // TODO
                }
            }
        });
    
        // ...
    
        @Override
        public void onClick(View v) {
            outputImage = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "output_image.jpg");
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                // 使用 FileProvider 生成 Content URI
                imageUri = FileProvider.getUriForFile(this, "com.amie.cameraalbum.FileProvider", outputImage);
            } else {
                imageUri = Uri.fromFile(outputImage);
            }
            // 以调用相机拍照为例
            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            // 设置 Extra 指定输出到对应的 Uri 上,固定写法
            // 可以在 AOSP 的 Camera 源码中可以找到答案
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
            //设置临时的读写权限
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            launcher.launch(intent);    // 使用 ActivityResultLauncher 启动 Intent
        }
    }
    

最后,如果需要兼容 Android 4.4 之前的系统,那么还要在 AndroidManifest 文件中声明 android.permission.WRITE_EXTERNAL_STORAGE 权限,Android 4.4 版本开始不再需要声明该权限。

 类似资料: