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 动态权限申请从未如此简单

相关推荐
guoruijun_2012_44 小时前
fastadmin多个表crud连表操作步骤
android·java·开发语言
Winston Wood4 小时前
一文了解Android中的AudioFlinger
android·音频
B.-5 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
有趣的杰克5 小时前
Flutter【04】高性能表单架构设计
android·flutter·dart
大耳猫11 小时前
主动测量View的宽高
android·ui
帅次13 小时前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛14 小时前
Android中Crash Debug技巧
android
kim565919 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼19 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ19 小时前
Android Studio使用c++编写
android·c++