I am using Storage Access Framework for Image Picker in my app. Below is the code
val types = arrayOf("image/png", "image/jpeg", "image/jpg")
val intent = Intents.createDocumentIntent(types, true)
if (canDeviceHandle(intent)) caller.startActivityForResult(intent, OPEN_GALLERY)
Here is the intent for creating document
fun createDocumentIntent(types: Array<String>, allowedMultiple: Boolean): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = if (!types.isNullOrEmpty()) {
putExtra(Intent.EXTRA_MIME_TYPES, types)
types[0]
} else "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowedMultiple)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
This is in OnActivityResult
private fun handleGalleryActivityResult(data: Intent?, callbacks: FilePicker.Callbacks) {
if (data == null) return
val files = mutableListOf<Uri>()
when {
data.clipData != null -> {
val clipData = data.clipData ?: return
(0 until clipData.itemCount).forEach { files.add(clipData.getItemAt(it).uri) }
}
data.data != null -> {
files.add(data.data!!)
}
else -> return
}
files.forEach {
val flags = data.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION
activity.contentResolver.takePersistableUriPermission(it, flags)
}
callbacks.onFilesPicked(files)
}
I am getting crash in line
activity.contentResolver.takePersistableUriPermission(it, flags)
in onActivityResult.
I read many solutions regarding this crash like adding persistable (FLAG_GRANT_PERSISTABLE_URI_PERMISSION) flag or adding takePersistableUriPermission but I have already have this but still I am getting this crash . I couldn't find any solution till now and my app users are facing this issue also on my phone I am not able to reproduce it myself.
Also on side note: I am using target version -> 11
Replace:
val flags = data.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION
with:
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
The only values that you pass to takePersistableUriPermission() are FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION, and you have no idea what data.flags has in it.
Related
I want to get content (images & videos) of user selected folder. Code to select the folder is:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
or Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
startActivityForResult(intent, 1088)
And onActivityResult, I am persisting the permission(even I tested without restarting the device but it is not working):
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == Activity.RESULT_OK) {
if (requestCode == 1088) {
val selectedDirUri = data!!.data
grantUriPermission(packageName, selectedDirUri, (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION))
val takeFlags = (data.flags
and (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
contentResolver.takePersistableUriPermission(selectedDirUri!!, takeFlags)
}
}
}
And I am traversing through all files using:
val rootUri: Uri = Uri.parse(selectedDir)
val contentResolver: ContentResolver = context.contentResolver
var childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, DocumentsContract.getTreeDocumentId(rootUri))
val dirNodes: MutableList<Uri> = LinkedList()
dirNodes.add(childrenUri)
while (!dirNodes.isEmpty()) {
childrenUri = dirNodes.removeAt(0) // get the item from top
val c = contentResolver.query(childrenUri,
arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED),
null, null, null)
try {
while (c!!.moveToNext()) {
val docId = c.getString(0)
val fileName = c.getString(1)
val mimeType = c.getString(2)
val lastModified = c.getLong(3)
if (isDirectory(mimeType)) {
val newNode = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId)
dirNodes.add(newNode)
} else {
val newNode = DocumentsContract.buildDocumentUriUsingTree(rootUri, docId)
Logger.log("TAG", "========1 Images: $newNode")
val ***sourceFileUri*** = newNode.toString()
}
}
} finally {
closeQuietly(c)
}
}
Then I display images and videos using Glide, it is not displaying.
Even if I try to copy the image using below code:
val inputStream: InputStream? = context.contentResolver.openInputStream(***sourceFileUri***)
I am getting below error, the above line gives an error:
java.lang.SecurityException: com.android.externalstorage has no access
to content://media/external _primary/file/1000008384 at
android.os.Parcel.createException or Null(Parcel.java:2438) at 08
android.os.Parcel.createException(P arcel.java:2422) at
android.os.Parcel.readException(Par cel.java:2405) at
android.database.DatabaseUtils.rea dExceptionFromParcel(DatabaseUtil
s.java:190) at android.database.DatabaseUtils.rea dException
WithFileNotFoundExcepti on From Parcel(DatabaseUtils.java:15 3)
This is happening in Vivo, Oppo and specially in new Samsung phones only which has android OS 11 and 12. I am really frustrated, I tried all possible solution but not able to find any solution till now.
Any solution or advice would be really helpful and appreciated, please please help me.
Developing an Android app in Flutter targetting Android SDK 30+.
I want to read and write data (xml files) to something like:
/storage/emulated/0/CustomDirectory/example.xml
Reading around I guess I'm supposed to use Intent.ACTION_OPEN_DOCUMENT_TREE so I wrote a MethodChannel which allows me to open the SelectDialog just fine. (I've trimmed all the try-catch and error handling for brevity)
private fun selectDirectory() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
startActivityForResult(intent, 100)
}
#RequiresApi(Build.VERSION_CODES.Q)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val uri = data.data!!
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return uri.toString())
}
I can call this from Flutter, it opens the "Select Directory" dialog, and I can choose my CustomDirectory, which then returns me a Content URI of:
content://com.android.externalstorage.documents/tree/primary%3ACustomDirectory
How do I convert that into a Flutter Directory?
In Flutter, I can call Directory.fromUri(...) but that just throws
Unsupported operation: Cannot extract a file path from a content URI
So I'm a little unsure of where to go from here, do I need to change the flags of my Intent or am I doing something very wrong somewhere?
This is going to be a long answer and a lot of the code is specific to my use case so if someone wants to reuse it, you might need to tweak things.
Basically with the changes in Android 30+ I wasn't able to get permissions to write to a directory on the user's phone that wasn't my apps own directory without requesting the dreaded manage_external_storage.
I solved this by doing this with native Kotlin then calling those methods via an interface in Dart.
First starting with the Kotlin code
class MainActivity : FlutterActivity() {
private val CHANNEL = "package/Main"
private var pendingResult: MethodChannel.Result? = null
private var methodCall: MethodCall? = null
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
val handlers = mapOf(
"getSavedRoot" to ::getSavedRoot,
"selectDirectory" to ::copyDirectoryToCache,
"createDirectory" to ::createDirectory,
"writeFile" to ::writeFile,
)
if (call.method in handlers) {
handlers[call.method]!!.invoke(call, result)
} else {
result.notImplemented()
}
}
}
This sets up our MainActivity to listen for methods named in the setMethodCallHandler method.
There are plenty of examples you can find for how to implement basic IO functions in Kotlin so I won't post them all here, but an example of how to open a set a content root and handle the result:
class MainActivity : FlutterActivity() {
//...
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun selectContentRoot(call: MethodCall, result: MethodChannel.Result) {
pendingResult = result
try {
val browseIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(browseIntent, 100)
} catch (e: Throwable) {
Log.e("selectDirectory", " error", e)
}
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 100 && resultCode == RESULT_OK) {
val uri: Uri = data?.data!!
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return pendingResult!!.success(uri.toString())
}
return
}
//..
Now to invoke that code in Dart I created an interface named AndroidInterface and implemented
class AndroidInterface {
final _platform = const MethodChannel('package/Main');
final _errors = {
'no persist document tree': FileOperationError.noSavedPersistRoot,
'pending': FileOperationError.pending,
'access error': FileOperationError.accessError,
'exists': FileOperationError.alreadyExists,
'creation failed': FileOperationError.creationFailed,
'canceled': FileOperationError.canceled,
};
String? _root;
// invoke a method with given arguments
Future<FileOperationResult<String>> _invoke(
String method, {
bool returnVoid = false,
String? root,
String? directory,
String? subdir,
String? name,
Uint8List? bytes,
bool? overwrite,
}) async {
try {
final result = await _platform.invokeMethod<String>(method, {
'root': root,
'directory': directory,
'subdir': subdir,
'name': name,
'bytes': bytes,
'overwrite': overwrite,
});
if (result != null || returnVoid) {
final fileOperationResult = FileOperationResult(result: result);
fileOperationResult.result = result;
return fileOperationResult;
}
return FileOperationResult(error: FileOperationError.unknown);
} on PlatformException catch (e) {
final error = _errors[e.code] ?? FileOperationError.unknown;
return FileOperationResult(
error: error,
result: e.code,
message: e.message,
);
}
}
Future<FileOperationResult<String>> selectContentRoot() async {
final result = await _invoke('selectContentRoot');
// release currently selected directory if new directory selected successfully
if (result.error == FileOperationError.success) {
if (_root != null) {
await _invoke('releaseDirectory', root: _root, returnVoid: true);
}
_root = result.result;
}
return result;
}
//...
Which basically sends the requests via _platform.invokeMethod passing the name of the method, and the arguments to send.
Using a factory pattern you can implement this interface device running 30+ and use standard stuff for Apple and devices running 29 and below.
Something like:
abstract class IOInterface {
//...
/// Select a subdirectory of the root directory
Future<void> selectDirectory(String? message, String? buttonText);
}
And a factory to decide what interface to use
class IOFactory {
static IOInterface? _interface;
static IOInterface? get instance => _interface;
IOFactory._create();
static Future<IOFactory> create() async {
final component = IOFactory._create();
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
final sdkInt = androidInfo.version.sdkInt;
_interface = sdkInt > 29 ? AndroidSDKThirty() : AndroidSDKTwentyNine();
}
if (Platform.isIOS) {
_interface = AppleAll();
}
return component;
}
}
Finally, the implementation for 30+ could look like
class AndroidSDKThirty implements IOInterface {
final AndroidInterface _androidInterface = AndroidInterface();
#override
Future<void> selectDirectory(String? message, String? buttonText) async {
final contentRoot = await _androidInterface.getContentRoot();
//...
}
Hopefully, this is enough to get you started and pointed in the right direction.
I'm trying to retrive from the user a file that can be both image or a pdf, using
registerForActivityResult(ActivityResultContracts.GetContent()) { file: Uri ->
......
}.launch(<mimetypes>)
I've already tried "image/*|application/pdf" from another question's answer but it didn't work, is there any way to ask for multiple MIME types when using registerForActivityResult ?
Here is my sample code,tested in api 31
var resultGalleryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val intent: Intent? = result.data
if (intent != null) {
intent.data?.let { selectedImageUri ->
....
}
}
} else {
Timber.e(" >>> error selected image from gallery by intent")
}
}
}
fun galleryLauncher() {
val intent = Intent(Intent.ACTION_GET_CONTENT, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
type = "image/*"
action = Intent.ACTION_GET_CONTENT
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/jpeg", "image/png", "image/jpg"))
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
}
resultGalleryLauncher.launch(intent)
}
is there any way to ask for multiple MIME types when using registerForActivityResult ?
Not directly with the current version of ActivityResultContracts.GetContent. However, you should be able to subclass it, override createIntent(), and from there customize the generated Intent. You can then try adding EXTRA_MIME_TYPES to that Intent with a String[] of the additional MIME types that you want.
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")
}
}
}
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)
}
}
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.
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.
I am currently trying to save a text file inside a fragment, but I can't get it to work:
Here is the method called when the user clicks the save button
private fun saveText(){
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_TITLE, "text.txt")
}
startActivityForResult(intent, 1)
}
Here is the onActivityResult method:
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
try {
val path = resultData?.data?.path
Log.wtf("Path", filePath)
val writer: Writer = BufferedWriter(FileWriter(path))
writer.write("Example Text")
writer.close()
} catch (e: Exception){
e.printStackTrace()
}
}
}
I also have the permissions set in the manifest and the file itself is created, but nothing is written. Perhaps I'm writing to the file wrong?
The error thrown is FileNotFoundException, because its trying to use a file from /document when I'm selecting one from /downloads
Suggested solution which unfortunately doesn't work:
resultData?.data?.let {
requireActivity().contentResolver.openOutputStream(it).use { stream ->
stream!!.bufferedWriter().write("Example Text")
}
}
A Uri is not a file.
Replace:
val path = resultData?.data?.path
Log.wtf("Path", filePath)
val writer: Writer = BufferedWriter(FileWriter(path))
writer.write("Example Text")
writer.close()
with:
resultData?.data?.let { contentResolver.openOutputStream(it).use { stream ->
stream.writer().write("Example Text")
}
}