如何在Jetpack Compose中实现SAF:权限校验、授权文件夹存储与复用
Android SAF 授权校验、存储与使用指南
针对你需要对Android/Media目录进行文件操作的需求,下面分三步解决SAF授权的校验、存储及后续使用问题,同时完善你提供的Compose代码:
一、校验SAF是否已授权
应用重启后需先验证之前保存的授权URI是否仍有效:
- 从持久化存储(如SharedPreferences)读取保存的URI字符串
- 通过
ContentResolver.checkUriPermission校验该URI是否拥有读写权限 - 若权限有效则直接使用;若无效或未存储,则引导用户重新授权
二、存储已授权的文件夹URI
SAF授权的URI必须持久化存储,否则应用重启后权限会丢失,推荐用SharedPreferences实现:
- 用户选择文件夹并授权后,将URI转为字符串存入SharedPreferences
- 同时调用
ContentResolver.takePersistableUriPermission获取持久化权限,避免权限失效
三、使用已授权的文件夹进行操作
通过DocumentFile.fromTreeUri将授权URI转为DocumentFile对象,即可执行复制、删除等文件操作:
- 删除文件:调用
DocumentFile.delete() - 复制文件:通过
ContentResolver.openInputStream读取源文件,openOutputStream写入目标位置
完善后的Compose代码示例
@Composable fun HomeScreen() { PermissionHandler { MediaScreen() } } @OptIn(ExperimentalPermissionsApi::class) @Composable fun PermissionHandler(content: @Composable () -> Unit) { val context = LocalContext.current val prefs = remember { context.getSharedPreferences("SAF_PREFS", Context.MODE_PRIVATE) } val permissionsList = listOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE ) // 从SharedPreferences读取已保存的URI val savedUriString = prefs.getString("AUTHORIZED_MEDIA_URI", null) val uri = remember { mutableStateOf(savedUriString?.let { Uri.parse(it) }) } val permission = rememberMultiplePermissionsState(permissions = permissionsList) // 校验URI权限是否有效 val isUriAuthorized = remember(uri.value) { uri.value?.let { val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.checkUriPermission(it, android.os.Process.myPid(), android.os.Process.myUid(), flags) == PackageManager.PERMISSION_GRANTED } ?: false } val safLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { selectedUri -> selectedUri?.let { // 获取持久化权限 val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.takePersistableUriPermission(it, flags) // 保存URI到SharedPreferences prefs.edit().putString("AUTHORIZED_MEDIA_URI", it.toString()).apply() uri.value = it } } if (permission.allPermissionsGranted) { if (!isUriAuthorized) { // 指定默认打开Android/Media目录 val mediaDirUri = DocumentsContract.buildRootUri( "com.android.externalstorage.documents", "primary:Android/media" ) Button(onClick = { safLauncher.launch(mediaDirUri) }) { Text(text = "请选择Android/Media文件夹") } } else { // 已授权,展示内容页面 content() } } else { AccessInfo( accessModel = permissionsList[0], isPopupEnabled = false, onDetailLinkClick = { /* 跳转到权限设置页逻辑 */ }, onPopUpButtonClick = { /* 取消逻辑 */ }, onAllowButtonClick = { permission.launchMultiplePermissionRequest() } ) } } // 示例:使用授权URI进行文件操作 fun deleteFileInMediaDir(context: Context, uri: Uri, fileName: String) { val mediaDir = DocumentFile.fromTreeUri(context, uri) mediaDir?.findFile(fileName)?.delete() } fun copyFileToMediaDir(context: Context, uri: Uri, sourceFile: File, destFileName: String) { val mediaDir = DocumentFile.fromTreeUri(context, uri) mediaDir?.createFile("*/*", destFileName)?.let { destFile -> context.contentResolver.openOutputStream(destFile.uri)?.use { outputStream -> sourceFile.inputStream().use { inputStream -> inputStream.copyTo(outputStream) } } } }
关键说明
- 默认目录跳转:使用
DocumentsContract.buildRootUri构建Android/Media的SAF标准URI,确保用户打开时直接定位到目标目录 - 权限持久化:必须调用
takePersistableUriPermission,否则应用重启后权限会失效 - 权限校验:每次启动应用都要通过
checkUriPermission确认权限有效性,避免因系统设置变更导致权限丢失
内容的提问来源于stack exchange,提问作者user13926345
相关产品推荐
相关产品推荐

