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

相关推荐
编程大师哥8 分钟前
Android Studio 2025 从性能优化到开发体验下载安装教程安装包
android·ide·android studio
我又来搬代码了8 分钟前
【Android】【Compose】Compose知识点复习(二)
android
Tom4i12 分钟前
【内存优化】使用 Android Studio Profiler 分析 .hprof 文件
android·android studio·内存优化·内存泄漏
summerkissyou198724 分钟前
Android-Audio-Usage 与 StreamType的区别
android·音视频
韩立学长32 分钟前
【开题答辩实录分享】以《智慧酒店管理——手机预订和住宿管理》为例进行选题答辩实录分享
android·java·后端
QT 小鲜肉33 分钟前
【Linux命令大全】001.文件管理之chgrp命令(实操篇)
android·linux·运维·笔记
_李小白40 分钟前
【Android FrameWork】第三十一天:Surface创建流程解析
android
柯南二号40 分钟前
【大前端】【Android】 Android 手机上导出已安装 App 的 APK
android·智能手机
Just_Paranoid42 分钟前
【Android UI】Android Tint 用法指南
android·ui·tint·porterduff·colorfilter
Android系统攻城狮1 小时前
Android16之交叉编译系统压力测试利器:stress-ng(二百六十六)
android·压力测试·android16·系统调试