Android 学习笔记 —— Content Provider 与 File Provider
Content Provider
Content Provider(内容提供器)主要用于在不同的应用程序之间实现数据共享的功能。它为应用程序存取数据提供统一的外部接口,它不同应用之间得以共享数据,同时还能保证被访问数据的安全性。
创建自定义 Content Provider
Android Studio 提供了快速创建 Content Provider 的方式,和 Broadcast Receiver 一样。
-
在对应包上右键 -> New -> Other -> Content Provider。填写 Provider 类名和 Uri Authorities。在创建时 Authorities 可先乱填,创建完成后再修改。
-
在 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>
-
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; } // ... }
-
上面的步骤完成后,这个 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。
使用步骤:
-
定义 FileProvider。
<!-- authorities:一个标识,在当前系统内必须是唯一值,一般用 <package_name>.FileProvider。 --> <!-- 这里使用官方提供的 FileProvider,name 规定为 androidx.core.content.FileProvider。推荐自定义 FileProvider --> <!-- grantUriPermissions:是否允许通过 Uri 授予文件的临时访问权限。必须设置为 true。 --> <!-- exported:表示是否对外公开。FileProvider 不需要也不应该对任何应用公开,为了安全必须为 false。 --> <provider android:authorities="com.amie.cameraalbum.FileProvider" android:name="androidx.core.content.FileProvider" android:grantUriPermissions="true" android:exported="false" />
-
在 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>
-
为定义的 FileProvider 添加文件路径。
<provider ... > <!-- 配置哪些路径可以通过 FileProvider 访问 --> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/> </provider>
-
为特定文件生成 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 版本开始不再需要声明该权限。