<p> 如今,安卓的老东家谷歌开始慢慢的对安卓的文件管理权限越来越重视了,从Android M开始,一些涉及到个人隐私的权限都需要显示的进行声明,那么AndroidN又做了哪些变化呢?<ol><li>APP应用程序的私有文件不再向使用者放宽</li><li>Intent组件传递 file://URI 的方式可能给接收器留下无法访问的路径,触发 FileUriExposedException 异常,推荐使用FileProvider</li><li>DownloadManager不再按文件名分享私人存储的文件。旧版应用在访COLUMN_LOCAL_FILENAME时可能出现无法访问的路径。面向 Android 7.0 或更高版本的应用在尝试访问 COLUMN_LOCAL_FILENAME 时会触发 SecurityException</li></ol></p>
<p> 在苹果上面,个人隐私权限一直都是备受重视的,尤其是文件的读写。至此,谷歌也开始对Android N的Uri进行了一些列的管理。下面我们来看看,Uri上到底发生了什么变化?
</p>
<p> 在Android中,我们最常用到的就是图片截图的功能,往往我们最常使用的就是系统提供的截图功能,如下:
</p>
Android裁图代码
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 2);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", outputX);
intent.putExtra("outputY", outputY);
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
intent.putExtra("return-data", false);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent, requestCode);
<p> 但是在Android N中,我们需要对Uri路径,尤其是带 file:// 的Uri要进行处理。
首先,我们需要在App的 Manifest.xml 中声明权限的 Provider:
</p>
Manifest.xml
<application ...>
...
<!-- 针对android N配置文件夹读取 -->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${application_id}.fileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
特别说明:
authorities:app的包名.fileProvider
grantUriPermissions:必须是true,表示授予 URI 临时访问权限
exported:必须是false
resource:中的@xml/file_paths是我们接下来要添加的文件
<p> 注意到配置中的一个xml文件 file_paths 了吗?这里是我们需要分配给应用读取文件路径的配置文件。首先,我们应该在Res目录下创建xml文件夹,然后创建file_paths.xml文件。
</p>
file_paths.xml 配置如下:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!--<external-path path="Android/data/app的包名/" name="root_path" /> -->
<external-path path="." name="external_storage_root" />
</paths>
其他说明:
path:需要临时授权访问的路径(.代表所有路径)
name:就是你给这个访问路径起个名字
<paths>根标签下可以添加的子标签也是有限的,参考官网的开发文档,除了上述的提到的<files-path>这个子标签外,还包括下面几个:<ol><li><cache-path>,表示应用默认缓存根目录,对应根目录等价于getCacheDir(),查看完整路径:/data/user/0/cn.teachcourse.demos/cache</li><li><external-path>,表示外部内存卡根目录,对应根目录等价于
Environment.getExternalStorageDirectory(),
查看完整路径:/storage/emulated/0</li><li><external-files-path>,表示外部内存卡根目录下的APP公共目录,对应根目录等价于
Context#getExternalFilesDir(String) Context.getExternalFilesDir(null),
查看完整路径:
/storage/emulated/0/Android/data/cn.teachcourse.demos/files/Download</li><li><external-cache-path>,表示外部内存卡根目录下的APP缓存目录,对应根目录等价于Context.getExternalCacheDir(),查看完整路径:
/storage/emulated/0/Android/data/cn.teachcourse.demos/cache</li></ol>最终,在file_provider.xml文件中,添加上述5种类型的临时访问权限的文件目录,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--
1、name对应的属性值,开发者可以自由定义;
2、path对应的属性值,当前external-path标签下的相对路径
比如:/storage/emulated/0/92Recycle-release.apk
sdcard路径:/storage/emulated/0(WriteToReadActivity.java:176)
at cn.teachcourse.nougat.WriteToReadActivity.onClick(WriteToReadActivity.java:97)
at android.view.View.performClick(View.java:5610)
at android.view.View$PerformClick.run(View.java:22265)
相对路径:/
-->
<!--1、对应内部内存卡根目录:Context.getFileDir()-->
<files-path
name="int_root"
path="/" />
<!--2、对应应用默认缓存根目录:Context.getCacheDir()-->
<cache-path
name="app_cache"
path="/" />
<!--3、对应外部内存卡根目录:Environment.getExternalStorageDirectory()-->
<external-path
name="ext_root"
path="pictures/" />
<!--4、对应外部内存卡根目录下的APP公共目录:Context.getExternalFileDir(String)-->
<external-files-path
name="ext_pub"
path="/" />
<!--5、对应外部内存卡根目录下的APP缓存目录:Context.getExternalCacheDir()-->
<external-cache-path
name="ext_cache"
path="/" />
</paths>
<p> 配置说明完了,那么我们应该如何使用这一临时权限呢?请继续往下翻页。。。</P>
Uri uri;
bool isSDKIntBigThanN = Build.VERSION.SDK_INIT >= Build.VERSION_CODES.N;
if(isSDKIntBigThanN){
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
}else{
uri = Uri.from(file);
}
Intent intent = new Intent("com.android.camera.action.CROP");
if(isSDKIntBiggerThanN){
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION|Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加相对应的读写权限的标识
}
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 2);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", outputX);
intent.putExtra("outputY", outputY);
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
intent.putExtra("return-data", false);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent, requestCode);
Ps: 裁图如果需要裁剪更清晰的图片,需要注释一上多个参数:
outputX, outputY, scale。因为需要更清晰的图片,又因考虑到图片占用内存较大,所以return-data必须写为false。
大致的问题都可以解决了,注意Uri的使用需要临时访问权限。如还不够明白,请参考:更详细的文档