0.系统权限更改
为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访问(0700)。此设置可防止私有文件的元数据泄漏,如它们的大小或存在性。此权限更改有多重副作用:
-
私有文件的文件权限不应再由所有者放宽,为使用 MODE_WORLD_READABLE 和/或 MODE_WORLD_WRITEABLE 而进行的此类尝试将触发 SecurityException。
注:迄今为止,这种限制尚不能完全执行。应用仍可能使用原生 API 或 File API 来修改它们的私有目录权限。但是,我们强烈反对放宽私有目录的权限。 -
传递软件包网域外的 file:// URI 可能给接收器留下无法访问的路径。因此,尝试传递 file:// URI 会触发 FileUriExposedException。分享私有文件内容的推荐方法是使用 FileProvider。
-
DownloadManager 不再按文件名分享私人存储的文件。旧版应用在访问 COLUMN_LOCAL_FILENAME 时可能出现无法访问的路径。面向 Android 7.0 或更高版本的应用在尝试访问 COLUMN_LOCAL_FILENAME 时会触发 SecurityException。通过使用 DownloadManager.Request.setDestinationInExternalFilesDir() 或 DownloadManager.Request.setDestinationInExternalPublicDir() 将下载位置设置为公共位置的旧版应用仍可以访问 COLUMN_LOCAL_FILENAME 中的路径,但是我们强烈反对使用这种方法。对于由 DownloadManager 公开的文件,首选的访问方式是使用ContentResolver.openFileDescriptor()。
来自 <https://developer.android.com/about/versions/nougat/android-7.0-changes.html>
在Android7.0以上版本系统中,如果应用尝试通过[file://]形式的uri直接访问其它应用程序目录下的文件;或者通过FileProvider,在未经授权、或被访问APP未提供授权访问功能的情况下,都将收到SecurityException异常。在使用过程中,会出现第三方APP会crash,或者无法得到文件数据等现象。
当然这些问题都是可以解决的。本文将一步一步描述如何写出兼容7.0以上系统的拍照->裁剪or选择图片->裁剪功能。
1.拍照
1.1.授权
private void judgePermission() { try { if (ContextCompat.checkSelfPermission( this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) && ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) && ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { Snackbar.make(getWindow().getDecorView(), "需要相机和存储权限", Snackbar.LENGTH_INDEFINITE).setAction("确认", new View.OnClickListener() { @Override public void onClick(View v) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST); } }).show(); } else { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST); } } else { //TODO //capturePicture();//拍照 } } catch (Exception e) { e.printStackTrace(); } }
1.2.调用系统相机拍摄
private void capturePicture() { final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (intent.resolveActivity(getPackageManager()) != null) {//使用判断是否存在裁剪Activity File file = null; try { file = createImageFile();//创建文件 } catch (IOException e) { e.printStackTrace(); } if (file != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0以上用FileProvider比较好 //高版本上使用FileProvider //为一个文件生成Content URI mUriForFile = FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file); } else { //5.0以下,不使用FileProvider,根据文件得到URI mUriForFile = Uri.fromFile(file); } intent.putExtra(MediaStore.EXTRA_OUTPUT, mUriForFile);//拍摄的照片输出路径uri startActivityForResult(intent, CAPTURE_IMAGE_REQUEST); } }
创建介质文件的公用代码:
private File createImageFile() throws IOException { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { File dir = new File(IMAGE_HEAD_FILE_PATH); if (!dir.exists()) { dir.mkdirs(); } final File nomedia = new File(IMAGE_HEAD_FILE_PATH + ".nomedia"); if (!nomedia.exists()) { nomedia.createNewFile(); } final File file = new File(IMAGE_HEAD_FILE_PATH + System.currentTimeMillis() + ".jpg"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { //如果是低版本,则根据文件生成uri mUriForFile = Uri.fromFile(file); } return file; } return null; }
2.从相册选择
启动一个action为Intent.ACTION_GET_CONTENT的Activity,type为”image/*”,这里的Intent.createChooser(intent,”选择图片”)作用是如果有多个图片选择器,
系统会弹出一个对话框供用户选择,对话框的标题为参数中传入的字符串。
public void pickPicture() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent,"选择图片"), PICK_IMAGE_REQUEST); }
3.裁剪
3.1.取Activity返回数据
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case CAPTURE_IMAGE_REQUEST://拍照的Activity响应 if (resultCode == RESULT_OK && mUriForFile != null) { cropImage(mUriForFile); } break; case PICK_IMAGE_REQUEST://本地选取的Activity响应 if (resultCode == RESULT_OK && data != null && data.getData() != null) { Uri uri = data.getData(); cropImage(uri); } break; case CROP_IMAGE_REQUEST://处理裁剪后的图片,这里是显示出来 showImage(mUriForFile); break; default: break; } }
3.2.对返回的图片进行裁剪
/** * 裁剪 * * @param fromUri 图片uri. */ private void cropImage(Uri fromUri) { if (fromUri == null) { return; } final Intent cropIntent = new Intent("com.android.camera.action.CROP"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //5.0以下部分设备的没权限修改其它APP下的文件 //如果不是程序自己的fileprovider,则把来源content uri的文件复制到自己fileprovider对应的目录下。 // 因为部分应用不允许Intent.FLAG_GRANT_WRITE_URI_PERMISSION权限,强行申请该权限,会报安全异常。 // 如果不拷贝文件,就需要自己实现一个文件裁剪器。自己的裁剪器裁剪的文件放在自己目录对应的包下,不会存在编辑时权限问题。 if (!fromUri.getAuthority().equals(FILE_PROVIDER_AUTHORITY)) { try { final File copyTargetFile = createImageFile();// Uri targetUri = FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, copyTargetFile); FileUtils.copyFileFromProviderToSelfProvider(getApplicationContext(), fromUri, targetUri); fromUri = targetUri;//替换fromUri,之后的裁剪工作,裁剪的是我们程序FileProvider中的文件。 } catch (IOException e) { e.printStackTrace(); } } }//如果是低版本则跳过上面这一步 cropIntent.setDataAndType(fromUri, "image/*");//capture from camera cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); cropIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); cropIntent.putExtra("crop", "true"); cropIntent.putExtra("aspectX", 1); cropIntent.putExtra("aspectY", 1); cropIntent.putExtra("outputX", 500); cropIntent.putExtra("outputY", 500); cropIntent.putExtra("scale", true); cropIntent.putExtra("return-data", false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { File cropImage = null; try { cropImage = createImageFile();//裁剪后的文件 } catch (IOException e) { e.printStackTrace(); } if (cropImage != null) { mUriForFile = FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, cropImage); //给所有具备裁剪功能的APP授权临时权限。 List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(cropIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; //给每个APP授权 grantUriPermission(packageName, fromUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); grantUriPermission(packageName, mUriForFile, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } } } else { try { createImageFile();//5.0以下直接创建文件,并赋值mUriForFile为新创建文件路径 } catch (IOException e) { e.printStackTrace(); } } cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, mUriForFile);//裁剪后的图片被输出的路径的uri cropIntent.putExtra("noFaceDetection", true); cropIntent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); startActivityForResult(cropIntent, CROP_IMAGE_REQUEST); }
4.重点:FileProvider使用
FileProvider是解决7.0以上图片裁剪问题的核心。Google给出了FileProvider使用的几个步骤
4.1.在清单中定义FileProvider
<manifest> ... <application> ... <provider android:name="android.support.v4.content.FileProvider" android:exported="false" android:authorities="com.androidcycle.captureandcrop.capture.fileprovider" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/catpure_image" /> </provider> ... </application> </manifest>
android:name属性可以为android.support.v4.content.FileProvider,或者自定义的FileProvider的子类。
android:authorities属性跟普通Provider的用法一样。
android:grantUriPermissions这里必须为true
4.2.指定可用文件
在res/xml目录下创建catpure_image.xml文件用来指定FileProvider可提供的路径(文件名随意指定,需要在manifest调用)。
<paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="my_images" path="Android/data/com.androidcycle.captureandcrop/" /> </paths>
<paths>标签必须包含一下几种子标签之一:
<files-path name=“name” path=“path” /> |
内部存储的files目录,子目录跟Context.getFilesDir()一样 |
<cache-path name=“name” path=“path” /> |
内部存储的cache目录,子目录跟getCacheDir()一样 |
<external-path name=“name” path=“path” /> |
外置存储的根目录,子目录跟Environment.getExternalStorageDirectory()一样 |
<external-files-path name=“name” path=“path” /> |
外置存储的你的APP的存储区域,与Context.getExternalFilesDir(String)或Context.getExternalFilesDir(null)的路径相同 |
<external-cache-path name=“name” path=“path” /> |
外置存储的你的APP的cache区域,子目录跟Context.getExternalCacheDir()一样 |
name为被分享出去的隐含的子目录名称;
path为实际子目录路径。
4.3.为文件生成Content URI
File imagePath = new File("路径要跟xml文件中指定的一样"); File newFile = new File(imagePath, "default_image.jpg"); Uri contentUri = FileProvider.getUriForFile(getContext(), "跟manifest中一样的AUTHORITY", newFile);
4.4.为URI授予临时权限
调用 Context.grantUriPermission(package, Uri, mode_flags)方法:
//给所有具备裁剪功能的APP授权临时权限。 List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(cropIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; //给每个APP授权 grantUriPermission(packageName, fromUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); grantUriPermission(packageName, mUriForFile, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); }
给Intent添加Flag
cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); cropIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
*注意:在低版本的设备上,试图使用.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)申请文件访问权限时,目标程序会crash。
因此在低版本上还是使用file://,既高版本用FileProvider.getUriForFile生成的Uri,低版本使用Uri.fromFile生成的Uri作为拍照和裁剪的输出目标路径。
*源码:https://github.com/gongshoudao/CaptureDemo