You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 22:17:43