Android 获取存储权限后首次获取文件拿不到uri的问题

本篇是记录实践开发过程中遇到的一个小问题:获取存储权限后选择文件没有回调。

一、产生的问题是什么

项目中有一个需求是用户可以选择手机中保存的某格式文件上传到我们的云服务,代码写完后在Android11及以上的版本跑的挺好的,但是在Android10及以下的手机版本首次获取到存储权限后去选择文件上传都获取不到文件的uri

刚开始以为是手上华为手机鸿蒙系统3.0(Android10 API29)获取存储权限的问题,于是去网上去找解决方案,还真找到了一篇"像模像样"的文章:【FAQ】从存储权限看HarmonyOS 3.0中应用适配

但是这篇文章漏洞百出MANAGE_EXTERNAL_STORAGE权限是Android 11才有的,华为手机鸿蒙系统3.0(Android10 API29)在适配的时候压根就不会走Android 11的代码分支,伪代码如下:

kotlin 复制代码
fun requestStoragePermissions(isSuccess: () -> Unit = {}) {
    //判断权限
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        val permissionArray = arrayOf(
            Manifest.permission.READ_MEDIA_IMAGES,
            Manifest.permission.READ_MEDIA_AUDIO,
            Manifest.permission.READ_MEDIA_VIDEO
        )
        //大于等于Android 13
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        //Android 11 12 如果获取所有文件需要权限MANAGE_EXTERNAL_STORAGE
    } else {
        val permissionArray = arrayOf(
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE
        )
       //小于等于Android 10,华为手机鸿蒙系统3.0(Android10 API29)获取存储权限走这里
    }
}

因为华为手机鸿蒙系统3.0(Android10 API29)获取存储权限只会走最后的else,所以压根不需要MANAGE_EXTERNAL_STORAGE权限。

那问题可能在哪里?为什么在Android10的手机上只有首次会获取权限后拿不到文件呢?但是在Android10以上的手机就是正常的?有没有可能在Android10及以下的手机上提前注册ActivityResultLauncher去拿文件有问题?

二、解决问题的思路

这里就有一个悖论,获取文件的ActivityResultLauncher注册需要在onStart之前,但是我又需要先获取存储权限后再注册ActivityResultLauncher,此时早就过了onStart的生命周期...

于是这里就只能采用代理绕过生命周期注册的方法了。

三、解决办法

之前看过一篇文章Android 动态权限申请从未如此简单,确实比之前的封装简化很多,于是现学现用了。

在上面的需求中,需要动态注册权限,还要动态注册获取文件的Launcher,统一封装成方法,这里就直接贴代码了,原理请查看原作者博客:

kotlin 复制代码
private val nextLocalRequestCode = AtomicInteger()

private val nextKey: String
    get() = "activity_rq#${nextLocalRequestCode.getAndIncrement()}"

/**
 * 申请权限
 */
fun AppCompatActivity.requestPermission(
    permission: String,
    onPermit: () -> Unit,
    onDeny: (shouldShowCustomRequest: Boolean) -> Unit
) {
    if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
        onPermit()
        return
    }
    var launcher by Delegates.notNull<ActivityResultLauncher<String>>()
    launcher = activityResultRegistry.register(
        nextKey,
        ActivityResultContracts.RequestPermission()
    ) { result ->
        if (result) {
            onPermit()
        } else {
            onDeny(!ActivityCompat.shouldShowRequestPermissionRationale(this, permission))
        }
        launcher.unregister()
    }
    lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                launcher.unregister()
                lifecycle.removeObserver(this)
            }
        }
    })
    launcher.launch(permission)
}

/**
 * 申请权限组
 */
fun AppCompatActivity.requestPermissions(
    permissions: Array<String>,
    onPermit: () -> Unit,
    onDeny: (shouldShowCustomRequest: Boolean) -> Unit
) {
    var hasPermissions = true
    for (permission in permissions) {
        if (ContextCompat.checkSelfPermission(
                this,
                permission
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            hasPermissions = false
            break
        }
    }
    if (hasPermissions) {
        onPermit()
        return
    }
    var launcher by Delegates.notNull<ActivityResultLauncher<Array<String>>>()
    launcher = activityResultRegistry.register(
        nextKey,
        ActivityResultContracts.RequestMultiplePermissions()
    ) { result ->
        var allAllow = true
        for (allow in result.values) {
            if (!allow) {
                allAllow = false
                break
            }
        }
        if (allAllow) {
            onPermit()
        } else {
            var shouldShowCustomRequest = false
            for (permission in permissions) {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
                    shouldShowCustomRequest = true
                    break
                }
            }
            onDeny(shouldShowCustomRequest)
        }
        launcher.unregister()
    }
    lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                launcher.unregister()
                lifecycle.removeObserver(this)
            }
        }
    })
    launcher.launch(permissions)
}


/**
 * 申请获取手机里面的单格式文件
 */
fun AppCompatActivity.requestGetContent(
    mimeType: String,
    onGetUri: (Uri?) -> Unit
) {
    var launcher by Delegates.notNull<ActivityResultLauncher<String>>()
    launcher = activityResultRegistry.register(
        nextKey,
        ActivityResultContracts.GetContent()
    ) { uri ->
        onGetUri(uri)
        launcher.unregister()
    }
    lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                launcher.unregister()
                lifecycle.removeObserver(this)
            }
        }
    })
    launcher.launch(mimeType)
}

清单文件中注册存储权限:

kotlin 复制代码
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
    android:name="android.permission.MANAGE_EXTERNAL_STORAGE"    //这个权限有可能会导致App过不了审,过不了审就去掉
    tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

请求存储权限并获取手机里面的文件:

kotlin 复制代码
/**
 * 动态请求存储权限并获取文件
 */
fun requestStoragePermissionsAndGetFileUri() {
    //判断权限
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        //权限按需添加
        val permissionArray = arrayOf(
            Manifest.permission.READ_MEDIA_IMAGES,
            Manifest.permission.READ_MEDIA_AUDIO,
            Manifest.permission.READ_MEDIA_VIDEO
        )
        requestPermissions(permissionArray, onPermit = {
            requestGetContent("*/*"){uri ->
                //拿到文件uri了
            }
        }, onDeny = {
            //权限未全部同意
        })
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {   //如果不加MANAGE_EXTERNAL_STORAGE权限,那这个分支就不需要了
        if (Environment.isExternalStorageManager()) {
            requestGetContent("*/*"){uri ->
                //拿到文件uri了
            }
        } else {
            //去系统设置界面获取所有文件的权限
            val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
            intent.data = Uri.parse("package:$packageName")
            startActivityForResult(intent, 200)
        }
    } else {
        val permissionArray = arrayOf(
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE
        )
        requestPermissions(permissionArray, onPermit = {
            requestGetContent("*/*"){uri ->
                //拿到文件uri了
            }
        }, onDeny = {
            //权限未全部同意
        })
    }
}

因为写博客为了清晰明了就写一起了,实际项目建议拆开来写。其中注意MANAGE_EXTERNAL_STORAGE权限有可能过不了审,过不了审就去掉。

获取uri后需要拿文件的真实路径,可以看我的这篇博客:Android ActivityResultContracts.GetContent 真实的文件路径

参考了以下资料

Android 动态权限申请从未如此简单

相关推荐
OkeyProxy3 小时前
設置Android設備全局代理
android·代理模式·proxy模式·代理服务器·海外ip代理
刘志辉4 小时前
vue传参方法
android·vue.js·flutter
前期后期7 小时前
Android OkHttp源码分析(一):为什么OkHttp的请求速度很快?为什么可以高扩展?为什么可以高并发
android·okhttp
轻口味9 小时前
Android应用性能优化
android
全职计算机毕业设计9 小时前
基于 UniApp 平台的学生闲置物品售卖小程序设计与实现
android·uni-app
dgiij10 小时前
AutoX.js向后端传输二进制数据
android·javascript·websocket·node.js·自动化
SevenUUp11 小时前
Android Manifest权限清单
android
高林雨露11 小时前
Android 检测图片抓拍, 聚焦图片后自动完成拍照,未对准图片的提示请将摄像头对准要拍照的图片
android·拍照抓拍
wilanzai11 小时前
Android View 的绘制流程
android
INSBUG12 小时前
CVE-2024-21096:MySQLDump提权漏洞分析
android·adb