Write permissions not working - scoped storage Android SDK 30 (aka Android 11)

11.8k views Asked by At

Anyone else finding scoped-storage near-impossible to get working? lol.

I've been trying to understand how to allow the user to give my app write permissions to a text file outside of the app's folder. (Let's say allow a user to edit the text of a file in their Documents folder). I have the MANAGE_EXTERNAL_STORAGE permission all set up and can confirm that the app has the permission. But still every time I try

val fileDescriptor = context.contentResolver.openFileDescriptor(uri, "rwt")?.fileDescriptor

I get the Illegal Argument: Media is read-only error.

My manifest requests these three permissions:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

I've also tried using legacy storage:

<application
    android:allowBackup="true"
    android:requestLegacyExternalStorage="true"

But still running into this read-only issue.

What am I missing?

extra clarification

How I'm getting the URI:

view?.selectFileButton?.setOnClickListener {
            val intent =
                Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                    addCategory(Intent.CATEGORY_OPENABLE)
                    type = "*/*"
                    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                }
            startActivityForResult(Intent.createChooser(intent, "Select a file"), 111)
        }

and then

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 111 && resultCode == AppCompatActivity.RESULT_OK && data != null) {
        val selectedFileUri = data.data;
        if (selectedFileUri != null) {
            viewModel.saveFilename(selectedFileUri.toString())
            val contentResolver = context!!.contentResolver
            val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            contentResolver.takePersistableUriPermission(selectedFileUri, takeFlags)
            view?.fileName?.text = viewModel.filename
            //TODO("if we didn't get the permissions we needed, ask for permission or have the user select a different file")
        }
    }
}
3

There are 3 answers

12
blackapps On

On Android 11 and testing with API 30 emulators i found public folders like

Download, Documents, DCIM, Alarms, Pictures and such 

writable for my apps using classic file system paths.

Restricted to app's own files.

Further i found that files created by one app in this way were writeble by a different app using SAF.

12
CommonsWare On

In terms of your code:

  • None of your listed permissions have anything to do with ACTION_OPEN_DOCUMENT
  • Neither of the flags on your Intent belong there

Your real problem, though, is that you appear to be choosing media, such as from the Audio category. ACTION_OPEN_DOCUMENT guarantees that we can read from the content identified by the Uri, but it does not guarantee a writeable location. Unfortunately, MediaProvider blocks all write access, throwing the exception whose message you cited.

Quoting myself from the issue that I filed last year:

The problem is that we have no way of specifying on the ACTION_OPEN_DOCUMENT Intent that we intend to write and therefore want to limit the user to writable locations. Given that Android Q/R are putting extra emphasis on us migrating to the Storage Access Framework, this sort of feature is needed. Otherwise, all we can do is detect that we do not have write access (e.g., DocumentFile and canWrite()), then tell the user "sorry, I can't write there", which leads to a bad user experience.

I wrote a bit more about this problem in this blog post.

So, use DocumentFile and canWrite() to see if you are allowed to write to the location identified by the Uri, and ask the user to choose a different document.

0
Andrey Alexandrov On

You may try the code below. It works for me.

class MainActivity : AppCompatActivity() {

    private lateinit var theTextOfFile: TextView
    private lateinit var inputText: EditText
    private lateinit var saveBtn: Button
    private lateinit var readBtn: Button
    private lateinit var deleteBtn: Button

    private lateinit var someText: String
    private val filename = "theFile.txt"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (!isPermissionGranted()) {
            val permissions = arrayOf(WRITE_EXTERNAL_STORAGE)
            for (i in permissions.indices) {
                requestPermission(permissions[i], i)
            }
        }

        theTextOfFile = findViewById(R.id.theTextOfFile)
        inputText = findViewById(R.id.inputText)
        saveBtn = findViewById(R.id.saveBtn)
        readBtn = findViewById(R.id.readBtn)
        deleteBtn = findViewById(R.id.deleteBtn)

        saveBtn.setOnClickListener { savingFunction() }
        deleteBtn.setOnClickListener { deleteFunction() }
        readBtn.setOnClickListener {
            theTextOfFile.text = readFile()
        }

    }

    private fun readFile() : String{
        val rootPath = "/storage/emulated/0/Download/"
        val myFile = File(rootPath, filename)
        return if (myFile.exists()) {
            FileInputStream(myFile).bufferedReader().use { it.readText() }
        }
        else "no file"
    }

    private fun deleteFunction(){
        val rootPath = "/storage/emulated/0/Download/"
        val myFile = File(rootPath, filename)
        if (myFile.exists()) {
            myFile.delete()
        }
    }

    private fun savingFunction(){
        deleteFunction()
        someText = inputText.text.toString()
        val resolver = applicationContext.contentResolver
        val values = ContentValues()
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
            values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
            val uri = resolver.insert(MediaStore.Files.getContentUri("external"), values)
            uri?.let { it ->
                resolver.openOutputStream(it).use {
                    // Write file
                    it?.write(someText.toByteArray(Charset.defaultCharset()))
                    it?.close()
                }
            }
        } else {
            val rootPath = "/storage/emulated/0/Download/"
            val myFile = File(rootPath, filename)
            val outputStream: FileOutputStream
            try {
                if (myFile.createNewFile()) {
                    outputStream = FileOutputStream(myFile, true)
                    outputStream.write(someText.toByteArray())
                    outputStream.flush()
                    outputStream.close()
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }

        }
    }

    private fun isPermissionGranted(): Boolean {
        val permissionCheck = ActivityCompat.checkSelfPermission(this, WRITE_EXTERNAL_STORAGE)
        return permissionCheck == PackageManager.PERMISSION_GRANTED
    }

    private fun requestPermission(permission: String, requestCode: Int) {
        ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
    }
}