Android N正式版已经出来了,像L和M一样,N也对开发者有了新的要求。
这篇文章主要讲解笔者在开发过程中遇到的一个Android N中Uri调用的例子。
谷歌在Android官网中也对N的行为变更有了讲解。见 https://developer.android.com/about/versions/nougat/android-7.0-changes.html
其中对应用间共享文件有如下介绍:
对于面向 Android N 的应用,Android 框架执行的 StrictMode API 政策禁止向您的应用外公开 file:// URI。 如果一项包含文件 URI 的 Intent 离开您的应用,应用失败,并出现 FileUriExposedException 异常。
若要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider 类。 如需有关权限和共享文件的更多信息,请参阅共享文件。
而我们一般拍照并指定照片存储位置的代码一般如下:
private void startTakePhoto() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File file = new File(Config.PHOTO_DIR + FileNameUtil.createFileName("png"));
file.setWritable(true, true);
if (file.exists()) {
startTakePhoto();
} else {
if (!file.getParentFile().exists())
file.getParentFile().mkdirs();
try {
file.createNewFile();
this.photoSavePath = file.getAbsolutePath();
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
startActivityForResult(intent, REQ_TAKE_PHOTO);
} catch (IOException e) {
log().e("创建照片文件失败!", e);
toast("抱歉,无法创建照片文件!");
}
}
}
其中,FileNameUtil.createFileName(String ext)
为随机文件名生成方法,用于生成随机文件名。 Config.PHOTO_DIR
为照片存储目录 ,这里声明为 public static String PHOTO_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + "/sccss/";
也就是手机中DCIM下的sccss文件夹。
这段代码在Android M及以下版本(最低只测试到4.0,再低版本不做考虑)运行起来,即使Android N的最后一个preview版本也可以跑得起来。唯独Android N上调用之后就挂掉了。
报错信息如下:
android.os.FileUriExposedException: file:///storage/emulated/0/DCIM/sccss/1472883323385.png exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:832)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8909)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
...
Android官方对FileUriExposedException的介绍如下:
(https://developer.android.com/reference/android/os/FileUriExposedException.html)
The exception that is thrown when an application exposes a file:// Uri to another app.
This exposure is discouraged since the receiving app may not have access to the shared path. For example, the receiving app may not have requested the READ_EXTERNAL_STORAGE runtime permission, or the platform may be sharing the Uri across user profile boundaries.
Instead, apps should use content:// Uris so the platform can extend temporary permission for the receiving app to access the resource.
This is only thrown for applications targeting N or higher. Applications targeting earlier SDK versions are allowed to share file:// Uri, but it’s strongly discouraged.
意思就是不让用file:// 类型的Uri,转而使用content://格式的Uri来访问跨App的文件访问。
谷歌官方为此有写一个示例,地址:
https://github.com/googlesamples/android-ScopedDirectoryAccess
根据示例,修改拍照代码:
/** 调用系统拍照 */
private void takePhoto() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
requestTakePhoto();
else
startTakePhoto();
}
@TargetApi(24)
private void requestTakePhoto() {
String dirName = Environment.DIRECTORY_DCIM;
StorageVolume storageVolume = mStorageManager.getPrimaryStorageVolume();
Intent accessIntent = storageVolume.createAccessIntent(dirName);
startActivityForResult(accessIntent, REQ_ACCESS_DCIM);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case REQ_ACCESS_DCIM: //拍照时请求DCIM访问权限
if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
try {
dealPhotoUri(data.getData());
} catch (IOException e) {
e.printStackTrace();
log().e("处理图片Uri出错:", e);
}
}
break;
}
}
@TargetApi(24)
@SuppressWarnings("ResultOfMethodCallIgnored")
private void dealPhotoUri(Uri uri) throws IOException {
ContentResolver contentResolver = getContentResolver();
Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri,
DocumentsContract.getTreeDocumentId(uri));
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri,
DocumentsContract.getTreeDocumentId(uri));
File file = new File(Config.PHOTO_DIR + FileNameUtil.createFileName("png"));
if(!file.getParentFile().exists())
file.getParentFile().mkdirs();
if (!file.exists())
file.createNewFile();
log().d("docUri = " + docUri);
log().d("childrenUri = " + childrenUri);
String caikuDirId = null;
try (Cursor docCursor = contentResolver.query(childrenUri, DIRECTORY_SELECTION, null, null, null)) {
while (docCursor != null && docCursor.moveToNext()) {
log().d("dir name= " + docCursor.getString(docCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)));
if ("sccss".equals(docCursor.getString(docCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))))
caikuDirId = docCursor.getString(docCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID));
}
}
if (!TextUtils.isEmpty(caikuDirId)) {
log().d("caikuDirId: " + caikuDirId);
Uri caikuUri = DocumentsContract.buildDocumentUriUsingTree(docUri, caikuDirId);
log().d("caikuUri=" + caikuUri.toString());
Uri caikuDirUri = DocumentsContract.buildChildDocumentsUriUsingTree(caikuUri, caikuDirId);
log().d("caikuDirUri=" + caikuDirUri.toString());
String imgId = null;
try(Cursor fileCursor = contentResolver.query(caikuDirUri, DIRECTORY_SELECTION, null, null, null)) {
while(fileCursor != null && fileCursor.moveToNext()) {
log().d("file name= " + fileCursor.getString(fileCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)));
if (file.getName().equals(fileCursor.getString(fileCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)))) {
imgId = fileCursor.getString(fileCursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID));
}
}
}
if (!TextUtils.isEmpty(imgId)) {
log().d("caikuDirId: " + imgId);
Uri imgUri = DocumentsContract.buildDocumentUriUsingTree(caikuUri, imgId);
log().d("fileUri = " + imgUri);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
this.photoSavePath = file.getAbsolutePath();
intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
startActivityForResult(intent, REQ_TAKE_PHOTO);
} else {
log().d("图片文件未找到!");
toast("图片文件未找到!");
}
} else {
log().d("照片文件夹未找到!");
toast("照片文件夹未找到!");
}
}
再次运行,触发takePhoto()
方法,会弹出如下对话框:
点击允许
后,会调用代码中onActivityResult
方法并且resultCode
参数为RESULE_OK
,如果用户选择取消
,则reultCode
为RESULT_CANCELED
。
在例中图片存放地址为手机存储下的DCIM\sccss\ 下。
本例中已经处理Android M中规定调用相机所需要的相机权限运行时声明(非本例讨论内容,可参考谷歌示例中的easypermission: https://github.com/googlesamples/easypermissions)。
若权为测试本例中的内容而不想关心运行时权限的内容,可手动在手机’设置’–’应用’–’[应用名称]’–’权限’,在这里手动将相机权限打勾。
当用户授权过外部存储的访问权限后,’设置’–’应用’–’[应用名称]’–’存储’中会出现相关内容:
ps: 因本人很少写博客,描述中可能有所遗漏。若有问题,可留言或发Email: sccss@sccss.cn