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

10,229

Solution 1

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

Solution 2

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.

Share:
10,229
Kamala Phoenix
Author by

Kamala Phoenix

Updated on June 28, 2022

Comments

  • Kamala Phoenix
    Kamala Phoenix almost 2 years

    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")
            }
        }
    }
    
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    But isn't there a way to get the user to give us permission to write to a file? For clarity, I'm making a document text editor and I want the user to be able to pick whatever file they want to open and edit. Shouldn't there be a way to let the user give us permission to write to whatever file the user wants?
  • CommonsWare
    CommonsWare over 3 years
    @KamalaPhoenix: "But isn't there a way to get the user to give us permission to write to a file?" -- not via ACTION_OPEN_DOCUMENT. "I'm making a document text editor" -- that's not media. I have only ever been able to get this exception when choosing media (e.g., a music track from Audio). The very exception says "media is read-only". If your users choose from more traditional spots in the Storage Access Framework UI (e.g., "Internal storage"), you should not get this exception and you should be able to write to the requested location.
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    Weird. I'm getting this error when opening a markdown file (.md) from the "Internal Storage > Documents" folder.
  • CommonsWare
    CommonsWare over 3 years
    @KamalaPhoenix: That is strange and scary. What device is this? Can you provide the actual Uri that you are getting back?
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    This is a Google Pixel 3 running Android 11. The URI that I get in the onActivityResult is: content://com.android.providers.media.documents/document/doc‌​ument%3A22678. It's worth noting as well that when I use an emulator or a different phone running sdk 28 this all works fine. But using the android:requestLegacyExternalStorage="true" in sdk 30 does not help with these errors.
  • CommonsWare
    CommonsWare over 3 years
    @KamalaPhoenix: "But using the android:requestLegacyExternalStorage="true" in sdk 30 does not help with these errors" -- it's not supposed to. That is for direct filesystem access, not the Storage Access Framework. I happen to have a Pixel 3 with 11 (though I'll have to see if it is on the final -- I was using it for the betas). I'll try to run an experiment tomorrow and will see what I see.
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    thanks so much! I really appreciate the help 🙏🏽 So is it that if I have that flag I can use something else to access files in the old 28 way? Or is SAF unavoidable? Just trying to understand what the 28 to 30 transition is supposed to look like.
  • blackapps
    blackapps over 3 years
    all we can do is detect that we do not have write access (e.g., DocumentFile and canWrite()). Maybe while taking persistable permission a write permission fails already in data.getFlags() and hence can not be taken..(Untested)
  • CommonsWare
    CommonsWare over 3 years
    @KamalaPhoenix: OK, using this sample app, I am getting the same basic results that you are, for files that were created by others (e.g., via user transferring file over USB). For documents created by the app (e.g., ACTION_CREATE_DOCUMENT), SAF write access works normally. I will experiment with a workaround in the coming days. "So is it that if I have that flag I can use something else to access files in the old 28 way?" -- until sometime next year, yes, when you raise targetSdkVersion to 30 or higher.
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    @CommonsWare thank you!! Literal lifesaver. Looking forward to what you find.
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    Could you clarify what you mean by "writable using classic file system paths"? And does that include files downloaded or USB-transfered onto the device?
  • blackapps
    blackapps over 3 years
    Well how did you read and write files in the old way? Without using uries or content providers or saf or mediastore? But using File and FileInput/OutputStream.
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    Ah. I was never able to figure out how let a user select a file and getting the File from it. The file selector seems to always give you back a Uri. Sounds like I need to look up an older way to select files.
  • blackapps
    blackapps over 3 years
    No. Not at all. Dont go back in time. All can be done using uries. If you get an uri using ACTION_OPEN_DOCUMENT then it is writable. If not check the obtained flags in inActivityResult. You try to take read and write but you could first check if you got read and write permission. Further remove the flags from your intent. You cannot grant anything. Instead you should be glad if anything is granted to you in onActivityResult.
  • CommonsWare
    CommonsWare over 3 years
    @blackapps: I cannot reproduce your findings for Documents/. I have not tried the other locations. I get the same exception on a Pixel 4 that Kamala does, when I try writing to a document transferred onto the device by the user via a USB cable. I suppose that the USB cable option could render the file writable by nothing, but that seems bizarre. Worse, our standard options for determining if we have write access all claim that we do, despite the fact that it fails.
  • CommonsWare
    CommonsWare over 3 years
    @KamalaPhoenix: Sorry, but my hoped-for workaround failed. For media Uri values, there is a way to request write permission from the user. I had hoped that I could find a way to get the corresponding media Uri for a document Uri and request permission that way, but that is not working out. I will file a variety of bug reports on this, as there are several issues all tied up in this problem. I will also try blackapps' approach of using files created solely by apps, in case this is somehow a transfer-via-USB problem.
  • blackapps
    blackapps over 3 years
    @CommonsWare, try to create -new- files and directories in the public folders i mentioned. Runs on all api 30 emulators here.
  • CommonsWare
    CommonsWare over 3 years
    @blackapps: How completely bizarre. Yesterday, and up until 10 minutes ago, I could reproduce Kamala's problems. Now, all of a sudden, I can't. I don't know exactly what's going on. I suspect that there's a real problem, but I don't know how to reliably reproduce it now. :-(
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    @CommonsWare thanks! I appreciate the help. Let me know what you find. I'll do some experimenting as well on how to compile what I have so that it works on Android 11. Markor and other markdown text editors still work in Android 11. So there's gotta be some way to do what I want even if it involves going around SAF somehow? I'm not sure.
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    @CommonsWare that's so weird! Did you recently update the phone? I wonder if they caught the bug and solved it on their end? Can you still reproduce on an emulator? I can try on my end and see...
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    @CommonsWare So I had a similar situation to you where I couldn't reproduce my problems anymore. And then today I factory reset my phone and reinstalled the app and I'm back to having the same problems as before. Did you end up filing any bugs with Google? Is there somewhere I can file bugs?
  • CommonsWare
    CommonsWare over 3 years
    @KamalaPhoenix: "Did you end up filing any bugs with Google?" -- I don't think so, as I don't think I could ever repro the problem again. "Is there somewhere I can file bugs?" -- issuetracker.google.com is the home of these sorts of bugs. This probably would go to the "Framework" category. However, a key to these bug reports is having a reproducible test case. Without that, it is unlikely that you will get very far. Even with a reproducible test case, it's a struggle.
  • Kamala Phoenix
    Kamala Phoenix over 3 years
    @CommonsWare Oof. Yeah, this looks like a black hole. Ah! And now it's back to working. I'm noticing an issue where permissions seem to depend on how I get to the file I selected. If I go through the "Documents" shortcut folder in the "File" app, then I get a permissions error. If I go through "Internal storage" and navigate directly to the file, I don't get permissions error. Am I getting the file uri right? This is how I'm doing it: github.com/madCode/dailylog/blob/master/app/src/main/java/co‌​m/…
  • CommonsWare
    CommonsWare over 3 years
    @KamalaPhoenix: Your references there to "filename" are somewhat scary, but otherwise that seems reasonable. I have seen some differences in responses by SAF based on the starting point like you describe, though not this specific one. Can you ensure that your question is up to date, and also provide the specific stack trace that you're seeing?