如何通过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
相关产品推荐
相关产品推荐

