Write permissions not working - scoped storage Android SDK 30 (aka Android 11)
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
andcanWrite()
), 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.
Kamala Phoenix
Updated on June 28, 2022Comments
-
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 tryval 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 over 3 yearsBut 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 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 over 3 yearsWeird. I'm getting this error when opening a markdown file (.md) from the "Internal Storage > Documents" folder.
-
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 over 3 yearsThis is a Google Pixel 3 running Android 11. The URI that I get in the onActivityResult is:
content://com.android.providers.media.documents/document/document%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 theandroid:requestLegacyExternalStorage="true"
in sdk 30 does not help with these errors. -
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 over 3 yearsthanks 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 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 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 raisetargetSdkVersion
to 30 or higher. -
Kamala Phoenix over 3 years@CommonsWare thank you!! Literal lifesaver. Looking forward to what you find.
-
Kamala Phoenix over 3 yearsCould 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 over 3 yearsWell 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 over 3 yearsAh. 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 over 3 yearsNo. 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 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 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 mediaUri
for a documentUri
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 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 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 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 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 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 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 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/com/…
-
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?