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://
,例如 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 字符串进行了规定:
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 就需要在另一个应用中去实现。
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>
Android 7.0 开启了严格模式(StrictMode),Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。也就是说无法直接将一个 File Uri 共享给另一个程序进行使用。
FileProvider 是Android 7.0 出现的新特性,它是 ContentProvider 的子类,可以通过创建一个 Content URI 并赋予临时的文件访问权限来代替 File URI 实现文件共享。简单来说就是将自身内部文件暴露给其他应用,并授予临时的文件读写权限。常见使用场景有,调用相机拍照和图片裁剪、应用升级调用系统应用安装器安装 APK。
使用步骤:
定义 FileProvider。
<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 访问到的文件路径。
<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>
为定义的 FileProvider 添加文件路径。
<provider
... >
<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 版本开始不再需要声明该权限。