前言
记得刚学java的时候,那个体育老师就告诉我们在java的世界里万物皆对象。没错,我的java是体育老师教的!既然是对象,都会经历不断的进化,就像人类由最初的猿人慢慢的进化到如今的高等人类。Android的世界亦如此,经历一代又一代的迭代,如今Android已经推出了8.0的版本。不过目前大多数公司应该没适配8.0吧,国内的手机也没几个是8.0系统吧(我只是推测,因为我上个月买的华为荣耀V9还是7.0版本,已经是很新的手机了)。每一个版本的迭代,必定会带来一些技术上的变革,当然肯定是朝着更为人性化的方向变革。今天要说的就是Android7.0中的一个新特性——FileProvider。
问题描述
在Android7.0之前,第三方的应用可以访问我们自身应用的私有路径。比如做一些第三方分享,通过Intent的方式,分享一张图片。只需要通过图片的路径就可以将图片找到然后暴露给第三方平台,以供分享。但是,在Android7.0以后,出于保护用户隐私的考虑,第三方应用是不可以随便访问我们APP的私有路径的。这里说不能随便访问,也就是说还是可以访问的,只是会变得矜持些,不那么随便了。
这里就要引入FileProvider这个类了,字面意思就是文件提供者,就是提供图片或者视频文件的。那么这个类到底怎么使用呢?凡事靠对比,才能看出二者的差异,下面我就从两个使用场景来指出在7.0上这两个场景与之前版本的差异。
FileProvider的使用
一、拍照
1.准备工作:
(1) 既然是适配7.0的系统,那么首先需要在build.gradle中配置相应的SDK版本号,我的配置如下:
(2) 在清单文件中,需要加入相机权限和写权限,如下:
2.调用相机拍照:
(1) 7.0以前的调用方式:
private void takePhotoOld() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 调起相机的意图
if (intent.resolveActivity(this.getPackageManager()) != null) {
File file = new File(getCameraSavePath());
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
this.startActivityForResult(intent, 1000); // 为了验证图片存储成功我选择在onActivityResult方法中
将它设置到一个ImageView上,代码就不上了,很简单
}
}
// 照完的图片存储路径(可以自行定义)
private String getCameraSavePath() {
String sdcardPath = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
sdcardPath = Environment.getExternalStorageDirectory().getPath();
} else {
sdcardPath = Environment.getDataDirectory().getPath() + "/data/" + "com.demo.android";
}
initDataDirectory(sdcardPath + "/Demo/"); // 注意:这里必须对路径进行初始化,否则文件无法创建,导致照相之后无法返回Activity
return sdcardPath + "/Demo/Camera.png";
}
// 初始化路径
private void initDataDirectory(String path) {
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
}
以上就是在7.0之前调用系统相机拍照并保存图片的方法,下面再来看一下7.0以后是如何调用的。在上代码之前说一下,如果在7.0的手机上也调用以上方法调用系统相机拍照,会导致程序崩溃并抛出以下异常。
从字面意思理解就是暴露文件路径异常,验证了我开始说的吧,7.0以上系统不再那么随便的将自身的私有路径暴露给第三方应用程序了。有人问了,相机也是第三方应用程序吗,这个答案是这样,当然是啦!好了,那下面就看一下如何成功的在7.0以上系统调用系统相机进行拍照。(这里需要说到一点,就是关于6.0动态权限的问题,在这先忽略,讲解完7.0调用相机的方法之后我会写上针对目前系统都适配的完整的调用系统相机的代码)
(2) 7.0以后的调用方式:
1.首先,需要在清单文件中声明一个provider,如下:
// 在res下创建xml文件夹
之所以需要声明,是因为FileProvider是身为Android四大组件之一的ContentProvider的子类,因此需要在清单文件中声明。
2.刚才的声明的最后一行,看到了一个xml文件。这个文件的作用是为FileProvider提供可以暴露的路径,一旦一个路径在文件中被声明,那么就可以被FileProvider提供。下面看一下这个xml文件中的内容:
// 外部存储,本文案例用的就是这个,注意这里的path就是共享的图片路径(我前面设置的文件路径就是外部存储下的Demo文件夹),name代表使用这个字段去访问真实的文件路径
// 以下是path可以设置的其他根节点路径
// 设备根目录,等同于直接new File("/")
// 内部存储空间应用私有目录下的files/ 目录,等同于Context.getFilesDir() 所获取的目录路径
// 内部存储空间应用私有目录下的cache/ 目录,等同于Context.getCacheDir() 所获取的目录路径
// 外部存储空间应用私有目录下的files/ 目录,等同于Context.getExternalFilesDir(null) 所获取的目录路径
// 外部存储空间应用私有目录下的cache/ 目录,等同于Context.getExternalCacheDir()
应该很多朋友好奇为什么要将路径写到这么一个xml文件里。因为我们现在使用FileProvider来提供这个文件,而FileProvider是ContentProvider的子类,它用content:// Uri 代替了 file:/// Uri。因此需要通过path以及name一起来供FileProvider来找到文件的位置。这样也更加安全的向第三方程序提供文件内容了。
3.接下来,就来看看FileProvider类是如何帮助我们来调用系统相机的:
private void takePhoto7() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(this.getPackageManager()) != null) {
File file = new File(getCameraSavePath());
Uri uri = FileProvider.getUriForFile(this, "com.android.demo.fileProvider", file); // 主要就是这行代码,通过FileProvider获取文件的uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
this.startActivityForResult(intent, 1000);
}
}
getUriFromFile方法中的第二个参数就是我们在清单文件中声明的provider的authorities。(一定要一样)
4.在7.0的手机上运行后,就会发现成功的调用了系统的相机并实现了拍照。同时,可以打印出uri。我这个案例中的uri为:
content://com.android.demo.fileProvider/Camera/Camera.png
怎么样,是不是验证我上面说的。
(3) 针对全部机型适配后的方法:
1.前面说到了,没有对6.0机型的动态权限进行适配。如果没有这个权限处理的话,那么在6.0以上的机型上进行此操作等待你的将是崩溃,因为启用照相机需要访问照相机权限以及写权限。因此,这里在调用相机操作之前,应该先处理权限问题。在6.0系统发布不久后,网上关于动态权限处理这一块就出现了好多的开源库供使用,基本方法都差不多,这里我举出EasyPermissions这个开源项目做举例。(关于这个库的使用可以自行百度,很简单)
2.这里,我直接上代码,在代码中会有注释,保证每一步都能简单易懂(我只写了用到的方法):
public class MainActivity implements EasyPermissions.PermissionCallbacks {
// 权限请求的回调结果,有成功失败两种结果,分别对应granted和denied两种状态。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// 在这里将回调结果赋给该方法,然后此方法会根据权限请求的成功与否分别调用下面两个方法
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
// 当权限获取成功时,回调此方法
@Override
public void onPermissionsGranted(int requestCode, List perms) {
// 在这里,再来调用照相方法
if (requestCode == 1001) {
takePhoto();
}
}
// 当权限获取失败时,回调此方法
@Override
public void onPermissionsDenied(int requestCode, List perms) {
// 这里可以给用户一些提示信息,比如告诉用户无此权限无法正常使用相机功能
}
// 检查权限并调用相机的方法
private void checkPermissionAndTakePhoto() {
// 判断是否有相机和写权限
if (EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// 有的话直接调用照相方法
takePhoto();
} else {
// 没有的话去请求权限
EasyPermissions.requestPermissions(this, "请求相机和写入权限",
1001, // 这个请求码自定义
Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
}
// 调用相机拍照的方法
private void takePhoto() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 调用相机的意图
if (intent.resolveActivity(this.getPackageManager()) != null) {
File photoFile = new File(getCameraSavePath()); // 图片保存的文件
// 版本判断
if (Build.VERSION.SDK_INT >= 24) {
Uri uri = FileProvider.getUriForFile(this, "com.android.demo.fileProvider", photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
} else {
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));
}
startActivityForResult(intent, 1000);
}
}
}
3.嗯,以上就是完整的针对目前各个系统都能用的调用系统相机的方法了。主要就是掌握FileProvider的使用,以及6.0权限检查机制。
二、向第三方应用分享(图片或者视频)
(1) 描述:
不知道大家知不知道国外的一些比较出名的APP, 例如Instagram,Youtube,Twitter等。这些都是一些海外用户手机必备的一些软件。就像在国内微信,微信视频,微博一样非常的火热。那么通常海外的一些APP为了增加自己产品的知名度,通常会选择向这些APP渠道上发布自己产品的一些信息,也就是为我们常说的第三方分享。但是国内的一些APP分享一般都是提供相应的SDK以及调用文档,我们调用相应的API即可实现在对应的平台上分享文字图片或视频等。
然而,还有一种分享不需要调用相比之下比较繁琐的API接口,只需要通过Intent即可调起相应的客户端实现分享。很多产品的分享更多这个功能,就是利用Intent来调起可以实现相应分享类型的客户端的。下面,我就来说明一下具体的实现步骤,以及在这个过程中对FileProvider的使用。
(2) 调用第三方客户端分享
1.确定Intent的分享类型:
// 1.这里的第一个参数图片路径,一定要和上面相机图片的存储路径一样 也要在xml文件中声明。
2.这里用的FileProvider也和上面是一个。
public Intent makeShareIntent(String shareImagePath, String shareContent) {
Intent intent = new Intent(Intent.ACTION_SEND); // 指定Intent用于分享
File file = null;
if (!TextUtils.isEmpty(shareImagePath)) {
file = new File(shareImagePath);
}
if (file != null && file.exists()) {
if (Build.VERSION.SDK_INT >= 24) { // 7.0以上适配
Uri uri = FileProvider.getUriForFile(context, "com.android.demo.fileProvider", file);
intent.putExtra(Intent.EXTRA_STREAM, uri);
} else { // 7.0以下
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse(shareImagePath));
}
intent.setType("image/*"); // 设置分享的类型为分享图片(视频为video/*)
} else {
intent.setType("text/plain"); // 当路径为空将分享类型设为分享文字
}
intent.putExtra(Intent.EXTRA_TEXT, shareContent);
return intent;
}
(这里额外说明一点,其实可以不进行SDK的版本适配,所有的系统都用FileProvider获取Uri。但这是需要给这个Intent设置一个flag标记如下):
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); (如果不加在7.0以下的机型就会发生崩溃,具体原因可以自行研究,不多说了!)
2.寻找指定的第三方平台:
public void startShareActivity(Context context, String pkgName,
String shareImagePath, String shareContent) {
ResolveInfo resolveInfo = null; // APK文件信息类
Intent intent = makeShareIntent(shareImagePath, shareContent); // 指定分享类型的Intent,上面的方法
PackageManager packageManager = context.getPackageManager(); // 应用程序管理类
List apks = packageManager.queryIntentActivities(intent, 0); // 查询所有能分享此类型Intent的应用,
并为其附一个标记0
int apkNum = apks.size();
for (int i = 0; i < apkNum; i++) {
ResolveInfo info = apks.get(i);
if (pkgName.equals(info.activityInfo.packageName)) { // 查找与指定包名一致的应用
resolveInfo = info;
break;
}
}
ActivityInfo ai = resolveInfo.activityInfo; // 获取应用的节点信息
intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name)); // 获取应用包名和应用程序名
context.startActivity(intent); // 启动第三方应用进行分享
}
3.弹出所有能分享此类型的应用列表
public void showAllShareActivities(Context context, String shareImagePath, String shareContent) {
Intent intent = makeAllShareIntent(shareImagePath, shareContent); // 确定指定分享类型的Intent
context.startActivity(Intent.createChooser(intent, "Share")); // 调起所有能分享的应用菜单
}