本篇是记录实践开发过程中遇到的一个小问题:获取存储权限后选择文件没有回调。
一、产生的问题是什么
项目中有一个需求是用户可以选择手机中保存的某格式文件上传到我们的云服务,代码写完后在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 真实的文件路径