Content Provider 与 File Provider

Android 学习笔记 —— Content Provider 与 File Provider

  • Content Provider
    • 创建自定义 Content Provider
    • 访问 Content Provider
    • File Provider

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:///,例如 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..
    @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 添加 标签及内容。


<manifest ... >
    
    <queries>
        
        <package android:name="com.amie.providerserver" />
        
        <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。

    
    
    
    
    <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 访问到的文件路径。

    
    
    <paths>
        
        <external-path name="external_storage_root" path="." />
    
        
        <files-path name="files-path" path="." />
    
        
        <cache-path name="cache-path" path="." />
    
        
        <external-files-path name="external_file_path" path="." />
    
        
        <external-cache-path name="external_cache_path" path="." />
    
        
        
        <root-path name="root-path" path="" />
    paths>
    
  3. 为定义的 FileProvider 添加文件路径。

    <provider
        ... >
        
        <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 版本开始不再需要声明该权限。

你可能感兴趣的:(Android,学习笔记,android,java,学习)