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

如何通过File Streams与SAF持久访问单个文件并避免重复权限请求

问题:SAF URI权限持久化失败导致重启应用后崩溃

需求场景

  • 用户点击选择文件按钮,通过系统文件选择器选择文件
  • 应用将文件URI保存至内部存储
  • 应用读取文件并处理其内容
  • 用户下次打开应用时,直接从内部存储读取URI并访问文件内容,无需额外操作

核心目标

利用SAF访问单个文件无需权限的特性,持久化临时权限,实现无需额外权限请求的无缝体验。

当前问题

用户可正常选择文件,但重启应用后加载文件时崩溃,报错:

java.lang.SecurityException: Permission Denial: opening provider com.android.externalstorage.ExternalStorageProvider from ProcessRecord{...} requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs

现有Kotlin代码

[package and imports...]

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AppTheme {
                FileSelect()   // dialog for selecting a file
            }
        }
    }
}

@Composable
fun FileSelect() {
    // State variables
    val context = LocalContext.current
    var selectedFileUri by remember { mutableStateOf<Uri?>(null) }
    var fileContent by remember { mutableStateOf<String?>(null) }

    // Open file from URI, set fileContent, and write URI to internal storage
    val openFile = { uri: Uri? ->
        selectedFileUri = uri
        uri?.let {
            fileContent = readTextFromUri(context, it)
            try {
                takePersistableUriPermission(context, it)
            } catch (e: SecurityException) {
                e.printStackTrace()
            }
        }
        // Try writing to internal storage for future use
        try {
            val fos: FileOutputStream = context.openFileOutput("URILocation.txt", Context.MODE_PRIVATE)
            fos.write(selectedFileUri.toString().toByteArray()); fos.flush(); fos.close()
            Log.d("MainActivity", "Write URILocation.txt - success")
            
        } catch (e: IOException) {
            // Error writing to URILocation.txt
            Log.d("MainActivity", "Write URILocation.txt - failure")
            e.printStackTrace()
        }
    }

    // Launch file selector
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.GetContent(),
        onResult = { uri: Uri? -> openFile(uri) }
    )

    // Handle select file
    fun buttonOnClick(){
        try {
            // Try reading URILocation.txt
            val fin: FileInputStream = context.openFileInput("URILocation.txt")
            Log.d("MainActivity", "Read URILocation.txt - success")
            var a: Int; val temp = StringBuilder(); while (fin.read().also { a = it } != -1) { temp.append(a.toChar()) }
            val uriS = temp.toString()  // URI String
            fin.close()
            // URI URI Object
            val uri: Uri = Uri.parse(uriS)
            // Read file at URI
            openFile(uri)
        } catch (e: IOException) {
            // If no URILocation.txt, launch file picker
            launcher.launch("*/*")
        }
    }

    Surface {
        Column {
            // Select file button
            Spacer(modifier = Modifier.height(100.dp))
            Button(onClick = {
                buttonOnClick()
            }) {
                Text(text = "Select or Load File")
            }

            // Display file contents
            selectedFileUri?.let { uri ->
                Text(text = "Selected file: $uri")
                fileContent?.let { content ->
                    Text(text = content)
                    [Do something with the content...]
                }
            }
        }
    }
}

// Takes file URI -> returns nullable string of content
fun readTextFromUri(context: Context, uri: Uri): String? {
    return context.contentResolver.openInputStream(uri)?.use { inputStream ->
        BufferedReader(InputStreamReader(inputStream)).use { reader ->
            reader.readText()
        }
    }
}

// Grant persistable permissions to URI
fun takePersistableUriPermission(context: Context, uri: Uri) {
    val contentResolver: ContentResolver = context.contentResolver
    val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION

    // Check if the URI can be granted persistable permissions
    val takeFlagsSupported = DocumentsContract.isDocumentUri(context, uri)
    if (takeFlagsSupported) {
        try {
            contentResolver.takePersistableUriPermission(uri, takeFlags)
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    } else {
        // Handle the case where the URI does not support persistable permissions
        Log.d("MainActivity","Persistable permissions not supported for this URI: $uri")
    }
}

问题分析与解决方案

核心问题:使用了错误的Activity Result Contract

你当前用的ActivityResultContracts.GetContent()不支持持久化URI权限,它返回的是临时权限的URI,即便调用takePersistableUriPermission也无法将权限保留到应用重启后。必须使用ActivityResultContracts.OpenDocument()——这是SAF中专门用于获取可持久化权限的API。

关键修正步骤

1. 替换Activity Result Contract

把原来的GetContent()换成OpenDocument():

val launcher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.OpenDocument(),
    onResult = { uri: Uri? -> openFile(uri) }
)

OpenDocument()返回的是符合SAF规范的文档URI,天生支持持久化权限。

2. 简化权限获取逻辑

OpenDocument()返回的URI必然是文档URI,无需额外判断DocumentsContract.isDocumentUri,直接调用takePersistableUriPermission即可,同时按需申请权限(比如仅需要读权限就不用申请写权限):

fun takePersistableUriPermission(context: Context, uri: Uri) {
    val contentResolver = context.contentResolver
    // 按需设置权限,这里仅申请读权限
    val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    try {
        contentResolver.takePersistableUriPermission(uri, takeFlags)
        Log.d("MainActivity", "持久化权限已授予: $uri")
    } catch (e: SecurityException) {
        e.printStackTrace()
        Log.e("MainActivity", "授予持久化权限失败: ${e.message}")
    }
}

3. 优化URI存储与读取逻辑

当前的URI转字符串存储的方式本身没问题,但重启后读取URI时无需重复执行权限获取和文件写入操作,直接读取内容即可:
修改buttonOnClick函数:

fun buttonOnClick(){
    try {
        val fin = context.openFileInput("URILocation.txt")
        Log.d("MainActivity", "读取URILocation.txt成功")
        val uriS = fin.bufferedReader().readText()
        fin.close()
        val uri = Uri.parse(uriS)
        // 直接读取文件内容,无需调用openFile
        fileContent = readTextFromUri(context, uri)
        selectedFileUri = uri
    } catch (e: IOException) {
        // 没有存储的URI时,启动文件选择器
        launcher.launch(arrayOf("*/*"))
    }
}

同时调整openFile函数,仅在首次选择文件时写入URI和获取权限:

val openFile = { uri: Uri? ->
    uri?.let {
        selectedFileUri = it
        fileContent = readTextFromUri(context, it)
        try {
            takePersistableUriPermission(context, it)
            // 将URI写入内部存储
            context.openFileOutput("URILocation.txt", Context.MODE_PRIVATE).use { fos ->
                fos.write(it.toString().toByteArray())
                Log.d("MainActivity", "写入URILocation.txt成功")
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

为什么原方式不可行?

  • GetContent()属于旧的内容选取机制,返回的URI权限是临时的,仅在当前应用会话有效,无法通过takePersistableUriPermission持久化。
  • 只有SAF的ACTION_OPEN_DOCUMENT(对应OpenDocument() Contract)返回的URI,才会被系统记录应用的访问权限,重启后依然有效。

内容的提问来源于stack exchange,提问作者Omaro_IB

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 18:05:14