1、Android 文件存储系统发展历史
- 从1.0起, 访问外部存储就需要申请权限WRITE_EXTERNAL_STORAGE
- 从4.1开始如果想读外部存储也要再额外申请权限READ_EXTERNAL_STORAGE
- 在4.4之前,android仅仅有一个设备称为‘external storage’从4.4起,可能会有多个外部存储设备,而且不同的设备会有不同的访问控制。其中一个外部存储是’primary’ external storage, 而其他的一个或者多个称为’secondary’ external storage; 4.4版本里面对于’primary external storage‘写入不需要申请权限;对于’scondary external storage‘不能再更改,除了app-specific folder(即:Android/data/your.package.name/)
- Android在4.4上剥夺了第三方应用程序对SD Card的写权限,使得一大批的应用程序不能够再使用外置的SD Card,此举招来了骂声一片,很快就有人给Android提了一个Bug,要求重新允许第三方应用获得SD Card的写权限,并且最终有1682个人关注了这一Bug,迫于压力,Google终于在5.0上Android又打开了一扇通往SD Card的大门:
- 4.4 版本上Google给出的解决方案就是通过刚刚在4.4 (Kitkat)中引入的Storage Access Framework(SAF)来让第三方应用向用户申请外置SD Card的读写权限,这样一来既保持了对第三方应用访问SD Card的限制,又赋予了用户更大的自主选择权
- 5.0 基于SAF上还增加了选择目录并将其读写权限赋予应用的新功能
- 6.0 访问SD卡需要动态申请权限
- 7.0 不允许在App间使用file://的方式传递File,需要使用FileProvider
- 8.0 在安装 APK 文件时新增 未知来源安装权限,即
android.permission.REQUEST_INSTALL_PACKAGES
2、FileProvider介绍
file://
的方式,传递一个 File ,否者会抛出 FileUriExposedException 的错误,会直接引发 Crash。2.1、不引入support v4 使用FileProvider
FileProvider继承了ContentProvider,它关联的类非常少,我们可以不引入support v4而直接导入它关联的类即可。
1、FileProvider的路径可以在Android SDK路径下面的extras\android\m2repository\com\android\support 找到support-core-utils, 如下图所示:
2、解压support-core-utils-xxx-sources.jar 在android\support\v4\content里面可以找到FileProvider
3、FileProvider关联ContextCompat可以也一同拷贝出来。
这样单独剥离出FileProvider只关联到两部分:FileProvider和ContextCompat;其中ContextCompat会关联到一些不同版本的ContextCompat,例如ContextCompatKitKat,也一同拷贝出来即可。
2.2、Authorities
和ContentProvider一样,authorities是系统范围内用来唯一区分一个Provider实例的标识。所有的provider都会在系统中注册,如果有第二个具有相同authorities的provider去注册会注册失败。
2.3、授予临时的读写权限
在配置 provider 标签的时候,有一个属性 android:grantUriPermissions="true"
,它表示允许它授予 Uri 临时的权限。
当我们生成出一个 content://
的 Uri 对象之后,其实也无法对其直接使用,还需要对这个 Uri 接收的 App 赋予对应的权限才可以。
而这个授权的动作,提供了两种方式来授权:
1、使用 Context.grantUriPermission()
为其他 App 授予 Uri 对象的访问权限。
grantUriPermission()
方法包含三个参数,这三个参数都非常的好理解。
- toPackage :表示授予权限的 App 的包名。
- uri:授予权限的 content:// 的 Uri。
- modeFlags:前面提到的读写权限。
这种情况下,授权的有效期限,从授权一刻开始,截止于设备重启或者手动调用 Context.revokeUriPermission()
方法,才会收回对此 Uri 的授权。
2、配合 Intent.addFlags() 授权。
既然这是一个 Intent 的 Flag,Intent 也提供了另外一种比较方便的授权方式,那就是使用 Intent.setFlags()
或者 Intent.addFlag
的方式。
这种形式的授权,权限截止于该 App 所处的堆栈被销毁。也就是说,一旦授权,直到该 App 被完全退出,这段时间内,该 App 享有对此 Uri 指向的文件的对应权限,我们无法再主动收回此权限了。
3、一种权限异常场景
分析:遇到这种错误一般是分享给目标app的uri没有权限读取,但是我们已经给该uri用上面提到的2种方式赋予了权限,为啥还是没有权限读取呢?
原因:原因是赋予权限的App没有sd卡读写权限,所以它也就无法赋予权限给该Uri了。
解决方法:分享uri和赋予权限给uri前需要动态检查当前app是否有sd卡的权限。关于动态权限如何获取,参考这篇文章:android 6.0运行时权限的完整接入流程
2.4、FileProvider完整接入步骤
- 引入FileProvider(引入support-v4,或者单独拷贝出FileProvider类)
- manifest配置FileProvider
- 定义provider的文件路径映射资源
- 获取分享的文件uri,授予临时访问权限
2.4.1、引入FileProvider
2.1节已经介绍了不引入support-v4包使用FileProvider的方式
2.4.2、Manifest里面配置FileProvider
1 2 3 4 5 6 7 8 9 10 11 |
<!-- android 7.0 安装更新包需要FileProvider --> <provider android:name="com.xxxx.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true" tools:replace="android:authorities"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_path" /> </provider> |
注意exported需要设置为false,grantUriPermissions需要设置为true
2.4.3、配置文件路径映射资源
文件路径映射的一个主要目的是对外隐藏文件的绝对路径,用一种别名的方式访问复杂的文件路径。
不同的存储路径可以通过不同的tag标识.
<files-path>
内部存储 internal storage 的子目录.<external-path>
外部存储external storage 的子目录.<cache-path>
缓存子目录.
1 2 3 4 |
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="my_update" path="mine/download"/> </paths> |
2.4.4、获取文件Uri,并授予临时权限
使用FileProvider.getUriForFile方法获得指定path的uri,该方法有3个参数:
authority 代表你指定接收的FileProvider;
file 为一个File对象,是一个需要转换成content uri的本地文件。
举一个例子:把本地下载的apk发送给系统安装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Intent intent = new Intent(Intent.ACTION_VIEW); File apkFile = new File(path); Uri apkUri = null; //7.0之前调用系统安装,需要传schema为file的uri;7.0及7.0以上的版本需要用shema为content的uri if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { apkUri = FileProvider.getUriForFile(appContext, appContext.getPackageName()+".fileprovider", apkFile); } else { apkUri = Uri.fromFile(new File(path)); } intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); appContext.startActivity(intent); |
注意点:
1、我们在Manifest里面配置FileProvider时,设置的authorities通常会是applicationId,因为applicationId在系统内是唯一的,和authorities对系统内唯一的要求是一致的。而我们在生成uri的时候没有直接获取applicationId的方法,所以我们通常用context.getPackageName方法代替,它们的获取结果是相同的。
2、我们生成uri后需要赋予该uri临时读取权限,不然对方app无法访问该uri。具体方式是通过setFlags或addFlags方法设置Intent.FLAG_GRANT_READ_URI_PERMISSION
3、7.0之前的版本调用系统安装,需要传schema为file的uri;7.0及7.0以上的版本需要用shema为content的uri
3、一个完整的接入案例
考虑到6.0到8.0版本系统对存储和安装的一些优化,我们想要实现一个从下载到安装的逻辑并非那么顺利,包含以下几个注意点:
- 下载时需要动态申请外部存储的SD卡写权限
- 下载完成后发送本地uri给系统安装时需要配置FileProvider
- 发送Uri之前需要检查是否已经申请sd卡的权限,这样才能授予uri临时访问权限。不然系统安装器会提示“There was a problem parsing the package”
- 调用系统安装时需要动态申请Manifest.permission.REQUEST_INSTALL_PACKAGES权限
可以看到我们在这个过程中需要频繁申请权限,我们可以把权限申请简单封装一个方法,可以让使用方操作更简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
public static void operateWithPermissions(int sceneId, final PermissionCheckResult checkResult, final String... permissions) { List<String> needCheckPermissionList = filterGrantedPermissions(permissions); int needCheckPermissionSize = needCheckPermissionList.size(); if(needCheckPermissionSize == 0) { checkResult.onAllPermissionGranted(); } else { if(needRequestPermission()) { final String[] toCheckPermissionArr = new String[needCheckPermissionSize]; needCheckPermissionList.toArray(toCheckPermissionArr); PermissionUtils.requestPermissionsForResult(toCheckPermissionArr, new PermissionResultCallbackBridge() { @Override public void onGranted(String permission) { } @Override public void onDenied(String permission) { } @Override public void finished(Bundle data) { boolean hasAllPermissions = true; for(String perm : toCheckPermissionArr) { boolean hasPermission = data.getBoolean(perm, false); if(!hasPermission) { hasAllPermissions = false; break; } } if(hasAllPermissions) { checkResult.onAllPermissionGranted(); } else { checkResult.onPermissionDenied(); } } }, null, sceneId); } else { checkResult.onPermissionDenied(); } } } private static List<String> filterGrantedPermissions(String... permissions) { List<String> needCheckPermissionList = new ArrayList<String>(); for(String permission : permissions) { if(!PermissionUtils.checkPermission(permission)) { needCheckPermissionList.add(permission); } } return needCheckPermissionList; } public static boolean needRequestPermission() { final int TARGETSDKVERSION = AppContextHelper.appContext().getApplicationInfo().targetSdkVersion; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && TARGETSDKVERSION >= Build.VERSION_CODES.M) { return true; } else { return false; } } public interface PermissionCheckResult { void onAllPermissionGranted(); void onPermissionDenied(); } |