Android Q 适配之旅

由于Google强制取消android:requestLegacyExternalStorage="true",所以针对AndroidQ的适配已经迫在眉睫了,其中Android Q一项比较重要的变更就是在外部存储设备中为每个应用提供了一个“隔离存储沙盒”。任何其他应用都无法直接访问应用的沙盒文件,由于文件是应用的私有文件,因此不再需要任何权限即可在外部存储设备中访问和保存自己的文件。同时我们也无法直接访问手机的外部存储。
由于我针对图片|视频选择参考更多Android 10适配请移步
https://developer.android.com/about/versions/10/features
先介绍一下沙盒机制下的三种存储方式

1、context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);// 图片存储路径
2、context.getExternalFilesDir(Environment.DIRECTORY_MOVIES);// 视频存储路径
3、context.getExternalFilesDir(Environment.DIRECTORY_DIRECTORY_MUSIC);//音频存储路径

问题一·没有办法显示图片

图片加载失败无法正常显示图片 因为MediaStore.MediaColumns.DATA 字段在Q版本被标记已过时,原因上面有提到过任何其他应用都无法直接访问您应用的沙盒文件;所以在Q上此字段返回的类似/storage/emulated/0/DCIM/Camera/IMG_20200105_19064545.jpg将无权限访问,即使你用Fresco这些优秀的第三方框架也显示失败,你在Logcat过滤一下Fresco日志就会发现会报如下异常:
java.io.FileNotFoundException: open failed: EACCES (Permission denied)意思是找不到文件&无权限访问

这个解决方案也很简单我们只需要通过MediaStore.Files.FileColumns._ID拿到资源的Id进行拼接转成Uri的方式content://media/external/file/20246就能正常显示图片,代码如下:

 long id = data.getLong(data.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));
 String path = isAndroidQ ? getRealPathAndroid_Q(id) : data.getString(data.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA));
 /**
     * 适配Android Q
     *
     * @param id
     * @return
     */
    private String getRealPathAndroid_Q(long id) {
        return QUERY_URI.buildUpon().appendPath(ValueOf.toString(id)).build().toString();
    }

问题二·将文件保存至公共目录报错

Q版本上这个File cameraFile = new File("存储路径");就有讲究了,如果你直接传公共目录地址肯定是不行的因为你没权限访问,你只能传应用内地址ctx.getExternalFilesDir(Environment.DIRECTORY_PICTURES);把图片存储在应用沙盒内,但我们是需要存储到系统相册中所以显然这也不是我们所期望的,所以我们只能通过Uri来处理,这个Uri就不能通过parUri(this, cameraFile);来获得了,可以通过以下方式创建拍照或录视频Uri;

/**
 * 创建一条图片地址uri,用于保存拍照后的照片
 *
 * @param context
 * @return 图片的uri
 */
fun createImageUri(context: Context): Uri? {
    val imageFilePath: Array = arrayOf(null)
    val status = Environment.getExternalStorageState()
    val time: String = com.luck.picture.lib.tools.ValueOf.toString(System.currentTimeMillis())
    // ContentValues是我们希望这条记录被创建时包含的数据信息
    val values = ContentValues(3)
    values.put(MediaStore.Images.Media.DISPLAY_NAME, DateUtils.getCreateFileName("IMG_"))
    values.put(MediaStore.Images.Media.DATE_TAKEN, time)
    values.put(MediaStore.Images.Media.MIME_TYPE, PictureMimeType.MIME_TYPE_IMAGE)
    // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
    if (status == Environment.MEDIA_MOUNTED) {
        values.put(MediaStore.Images.Media.RELATIVE_PATH, PictureMimeType.DCIM)
        imageFilePath[0] = context.contentResolver
            .insert(MediaStore.Images.Media.getContentUri("external"), values)
    } else {
        imageFilePath[0] = context.contentResolver
            .insert(MediaStore.Images.Media.getContentUri("internal"), values)
    }
    return imageFilePath[0]
}

/**
 * 创建一条视频地址uri,用于保存录制的视频
 *
 * @param context
 * @return 视频的uri
 */

fun createVideoUri(context: Context): Uri? {
    val imageFilePath: Array = arrayOf(null)
    val status = Environment.getExternalStorageState()
    val time: String = com.luck.picture.lib.tools.ValueOf.toString(System.currentTimeMillis())
    // ContentValues是我们希望这条记录被创建时包含的数据信息
    val values = ContentValues(3)
    values.put(MediaStore.Video.Media.DISPLAY_NAME, DateUtils.getCreateFileName("VID_"))
    values.put(MediaStore.Video.Media.DATE_TAKEN, time)
    values.put(MediaStore.Video.Media.MIME_TYPE, PictureMimeType.MIME_TYPE_VIDEO)
    // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
    if (status == Environment.MEDIA_MOUNTED) {
        values.put(MediaStore.Video.Media.RELATIVE_PATH, PictureMimeType.DCIM)
        imageFilePath[0] = context.contentResolver
            .insert(MediaStore.Video.Media.getContentUri("external"), values)
    } else {
        imageFilePath[0] = context.contentResolver
            .insert(MediaStore.Video.Media.getContentUri("internal"), values)
    }
    return imageFilePath[0]
}

将生成的Uri传入 cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);这样在Q手机上拍照或录的视频则可以保存到/storage/emulated/0/DCIM/Camera/目录下了。

问题三·使用图片选择器在Q机型上传图片失败?

java.io.FileNotFoundException: open failed: EACCES (Permission denied) 无权限访问或文件找不到,这同样也是因为我们在选择公共目录下的一些图片资源上传时你并没有访问权限所以也自然不能使用它进行上传操作,其实我们在上面讲到MediaStore.MediaColumns.DATA`已过时问题时你点开源码就会发现其实已经告诉了我们替代方案了

/**
         *
         *   @deprecated Apps may not have filesystem permissions to 
         *   directly
         *   access this path. Instead of trying to open this path
         *   directly, apps should use
         *   {@link ContentResolver#openFileDescriptor(Uri, String)}
         *   to gain access.
         */ 

通过ContentResolver#openFileDescriptor(Uri, String);方法获得访问权限,PictureSelector的解决方法是
通过getContentResolver().openFileDescriptor(uri,"r");获得文件的访问权限然后将其拷贝到自己应用沙盒中然后再进行上传操作,代码如下:
在这里感谢@周咏龙-Yonglong Zhou 提供此方法

val parcelFileDescriptor: ParcelFileDescriptor =getContentResolver().openFileDescriptor(uri, "r")
val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor
val inputStream = FileInputStream(fileDescriptor)
val copyFileSuccess: Boolean = copyFile(inputStream, outFile)
if (copyFileSuccess) {
    Log.i(TAG, "Copy File Success")
}

/**
 * Copy File
 *
 * @param fileInputStream
 * @param outFile
 * @return
 * @throws IOException
 */
@Throws(IOException::class)
fun copyFile(fileInputStream: FileInputStream?, outFile: File?): Boolean {
    if (fileInputStream == null) {
        return false
    }
    var inputChannel: FileChannel? = null
    var outputChannel: FileChannel? = null
    return try {
        inputChannel = fileInputStream.getChannel()
        outputChannel = FileOutputStream(outFile).getChannel()
        inputChannel.transferTo(0, inputChannel.size(), outputChannel)
        inputChannel.close()
        true
    } catch (e: Exception) {
        false
    } finally {
        if (inputChannel != null) inputChannel.close()
        if (outputChannel != null) outputChannel.close()
    }
} 

问题四·什么版本需要请求读写权限

在AndroidQ是不需要申请读写权限的所以要在manifest中声明读写权限中声明请求权最大使用版本

android:maxSdkVersion="28"


有同学说自己通过Uri转Path也能拿到真实路径进行上传,在这里我想说的是Uri转Path其实早在AndroidQ未出来之前就一直可以的,这也仅仅是路径之间的相互转换而已并没有访问的权限,如果有,那么只能说明你应用 targetSdkVersion没有改成>=29或没有开启沙盒机制

@娄宇- Yu Lou 做Android11适配去掉android:requestLegacyExternalStorage="true" 在没有做任何适配情况下,没有出现任何问题是因为其他三方加入该方法导致合并manifest该方法仍然存在

以上是我整理的适配Android11方案,如果有不对的地方或有更好的适配方案还请各位大佬多多指点

你可能感兴趣的:(Android Q 适配之旅)