androidkotlinandroid-jetpack-compose

How to use File Streams and Storage Access Framework (SAF) to persistently access a single file without repeatedly asking for permissions


The idea is this:

The idea is to prevent any permission requests since SAF does not need permissions for accessing a single file. The temporary permissions are also persisted so next time the user opens the app, they don't need to do anything, allowing for a seamless experience.

However, I'm having some trouble persisting this permission. I'm thinking it has something to do with the way the URI is stored (it must be converted to a string before it can be written as a byte array), so maybe the permissions information is lost?

Right now, the user is able to select a file, but when the app is restarted and they try to load the file again, the app crashes due to 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

My code right now looks like this:

[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")
    }
}

Is it even possible to do it this way or am I missing something? Thanks!


Solution

  • Switch ActivityResultContracts.GetContent() to ActivityResultContracts.OpenDocument(). GetContent is not part of SAF; you cannot get persistable Uri permissions using GetContent.