Android SAF, cannot copy file, FLAG_SUPPORTS_COPY not set - android

I'm trying to copy a document with the SAF framework in Android by using DocumentsContract.copyDocument(ContentResolver, Uri, Uri) however this doesn't work, android returns error "Failed to copy document".
By narrowing the issue down, the FLAG_SUPPORTS_COPY is off on that document (according to DocumentsContract.Document#COLUMN_FLAGS)
(COLUMN_FLAGS value is 326 in decimal). So this explains the error.
However moving the document is allowed (flag FLAG_SUPPORTS_MOVE is on) and file is really moved when calling DocumentsContract.moveDocument(ContentResolver, Uri, Uri, Uri)
Access to the document tree (both the root of the drive, and DCIM folder) have been granted through Intent.ACTION_OPEN_DOCUMENT_TREE
Why is the FLAG_SUPPORTS_COPY set to false for the Document ? Am I missing something ?
Note: I believe I fullfill the requirements from this post https://stackoverflow.com/a/58147682/15401262
Thank you
Code (java)
// docFilesToProcess if of type "DocumentFile[]" and contains "regular files, like images" (not directories).
// Create destination dir
Uri destUri = DocumentsContract.createDocument(this.getContentResolver(), docFilesToProcess[i].getParentFile().getUri(), DocumentsContract.Document.MIME_TYPE_DIR, "destDir");
Log.i("M", "destUri: "+ destUri.toString());
// Create document
Uri docToMove = DocumentsContract.createDocument(this.getContentResolver(), docFilesToProcess[i].getParentFile().getUri(), "text/plain", "text");
Log.i("M", "docToMove: "+ docToMove.toString());
// copy document
DocumentsContract.copyDocument(this.getContentResolver(), docToMove, destUri);
Output
I/M: destUri: content://com.android.externalstorage.documents/tree/primary%3ADCIM/document/primary%3ADCIM%2FdestDir
I/M: docToMove: content://com.android.externalstorage.documents/tree/primary%3ADCIM/document/primary%3ADCIM%2Ftext.txt
W/DocumentsContract: Failed to copy document
java.lang.UnsupportedOperationException: Copy not supported
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:172)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
at android.content.ContentProviderProxy.call(ContentProviderNative.java:658)
at android.content.ContentResolver.call(ContentResolver.java:2042)
at android.provider.DocumentsContract.copyDocument(DocumentsContract.java:1442)
at com.example.exifthumbnailadder.MainActivity.addThumbs(MainActivity.java:1036)
at java.lang.reflect.Method.invoke(Native Method)
Persistant permission request
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);

In general, you cannot ever rely on any SAF provider to implement any optional feature. You should check whether the feature is supported, and you should have a viable fallback plan for when it is not.
ContentResolver cr = getContentResolver();
if((flags & FLAG_SUPPORTS_MOVE) == FLAG_SUPPORTS_MOVE)
Uri newDoc = DocumentsContract.copyDocument(cr, docToCopy, destDir);
else {
Uri newDoc = DocumentsContract.createDocument(cr, destDir, mimeType, name);
manuallyCopyBytes(docToCopy, newDoc);
}
It's important to give the provider a chance to do it, because something like Google Drive may be able to perform the copy separately on the device and on the server, instead of copying it on the device and then having to upload the whole thing to the server again.

Related

Can not create and listing file from sub-directory using Android SAF - ACTION_OPEN_DOCUMENT_TREE

In my app, I did code for selecting a directory with persistence permission using android ACTION_OPEN_DOCUMENT_TREE. I did everything successfully, but the problem is I can not create a file inside the sub-directory and cannot get a list of files from the sub-directory. It gives me errors like Permission Denial: writing com.android.externalstorage.ExternalStorageProvider uri
but according to android official doc, it says Your app can then access any file in the selected directory and any of its sub-directories.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
activity.startActivityForResult(intent, PERMISSION_CODE);
in onActivityResult
if (requestCode == PERMISSION_CODE) {
if (resultData != null) {
Uri treeUri = resultData.getData();
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
if(DocumentFile.fromTreeUri(context, treeUri).findFile("backup") == null){
Uri backupDirUri = DocumentFile.fromTreeUri(context, treeUri)
.createDirectory("backup").getUri();
//this statement gives me error
DocumentFile.fromTreeUri(context, backupDirUri)
.createFile("text/plain", "34234234.txt")
.getUri();
}
}
}
Suppose an app user select a directory inside SDcard named with MyFolder then I have created a directory backup inside MyFolder but I can not create a file inside the backup directory using the backup directory URI.
For some reason using getUri() on a DocumentFile instance and then attempt to create another DocumentFile from that Uri fails.
Solution: don't attempt to operate on a Uri. Work with the DocumentFile instance.
The first call to fromTreeUri must be using an Uri or Uri from String. But from that point create new files by using the returned DocumentFile.
For example, you can try this:
if (requestCode == PERMISSION_CODE) {
if (resultData != null) {
Uri treeUri = resultData.getData();
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
DocumentFile root = DocumentFile.fromTreeUri(context, treeUri);
if (root.findFile("backup") == null) {
DocumentFile backupDirUri = root.createDirectory("backup");
// this statement should not give error now
DocumentFile f = backupDirUri.createFile("text/plain", "34234234.txt");
// Write to your just created file.
// openOutputStream with f.getUri() will work at this point.
// For this example, plain OutputStream to write bytes,
// wrap it with writers or something more functional.
OutputStream os = context.getContentResolver().openOutputStream(f.getUri());
os.write("It works!".getBytes(Charset.forName("US-ASCII"));
os.flush();
os.close();
}
}
}
It's not only that going back and forward from DocumetFile to Uri then to DocumentFile again is very inefficient if done in the same function call, but that it always fails.
Why it fails?
Well... sorry, I don't know. I just know that in my test devices it fails. I don't know why.
You can safely going from Uri to DocumentFile in future iterations, or with files that already exists in the file system (I mean, not just created during that same function call).
You cannot go from Uri to DocumentFile if you just created a file during that same function call. But you can safely operate on the returned DocumentFile of the just created file. That's why you must remember the returned DocumentFile, not its Uri, if you plan to immediately operate on that just created file.
If you just want to operate on the file at a later time, then you can save the Uri. And you probably want to remember it, because I'm not sure of what happens to a DocumentFile instance once the app is suspended then resumed. In that case, Uri may be safer. Also the string representation of a Uri is what you can save to preferences or any kind of data base/custom file.
But again, if you want to immediately do something with the created file, don't try to get its Uri and then create a DocumentFile. Just use the DocumentFile you already got.
To me, it looks like DocumentFile.createDirectory and DocumentFile.createFile returns before the device file system is updated. This is a guess. I still has to hear the true explanation.
That would explain why you can operate with the returned DocumentFile instance, of a just created child file/directory, but if you save the Uri and try to call any DocumentFile function that accepts an Uri, with the Uri of a just created file/directory, then it fails.
Another possibility is that you cannot create a second DocumentFile while another refers to the same uri. Remember that you may not have references to it, but it exists until garbage collection.

Android (Xamarin) navigating folder tree through Storage Access Framework Uri

Attempting to navigate folder tree using Storage Access Framework and Uri, I am seeming some unexpected behavior. In short, my test case is a roundtrip DocumentFile -> Uri -> DocumentFile -> Uri test.
(code is for Xamarin but these are underlying Android classes so it should not be language specific.)
void Test(DocumentFile folder)
{
var uri = folder.Uri;
//
// Test case is using an SD card mounted in a card trader in a USB hub.
// uri: {content://com.android.externalstorage.documents/tree/1111-1111%3ADCIM/document/1111-1111%3ADCIM%2F110ND810}
//
var folder2 = DocumentFile.FromTreeUri(context, uri);
var uri2 = folder2.Uri;
//
// uri2: {content://com.android.externalstorage.documents/tree/1111-1111%3ADCIM/document/1111-1111%3ADCIM}
//
// At this point I expected uri2 to be equal to uri, but it's not.
// Instead, uri2 points to the parent of uri
//
}
Question 1: Are my expectations off here? Should I not expect to be able to navigate a SAF folder tree using Uri?
Question 2: If so, what is a suitable workaround?
The following library makes working with SAF so easy. I think you shouldn't get two different uri!
https://github.com/madnik7/PortableStorage

Creating a writeable DocumentFile from URI

I'm trying to adapt a File-based document system to something using DocumentFile in order to allow external storage read/write access on API >= 29.
I get the user to select the SD card root using Intent.ACTION_OPEN_DOCUMENT_TREE, and I get back a Uri as expected, which I can then handle using:
getContentResolver().takePersistableUriPermission(resultData.getData(),
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
I can browse successfully through the external storage contents up to the selected root. All good.
But what I need to be able to do is write an arbitrary file in the chosen (sub)folder, and that's where I'm running into problems.
DocumentFile file = DocumentFile.fromSingleUri(mContext, Uri.parse(toPath));
Uri uri = file.getUri();
FileOutputStream output = mContext.getContentResolver().openOutputStream(uri);
Except on the openOutputStream() call I get:
java.io.FileNotFoundException: Failed to open for writing: java.io.FileNotFoundException: open failed: EISDIR (Is a directory)
That's slightly confusing to me, but the "file not found" part suggests I might need to create the blank output file first, so I try that, like:
DocumentFile file = DocumentFile.fromSingleUri(mContext, Uri.parse(toPath));
Uri uri = file.getUri();
if (file == null) {
return false;
}
if (file.exists()) {
file.delete();
}
DocumentFile.fromTreeUri(mContext, Uri.parse(getParentPath(toPath))).createFile("", uri.getLastPathSegment());
FileOutputStream output = mContext.getContentResolver().openOutputStream(uri);
I get a java.io.IOException:
java.lang.IllegalStateException: Failed to touch /mnt/media_rw/0B07-1910/Testing.tmp: java.io.IOException: Read-only file system
at android.os.Parcel.createException(Parcel.java:2079)
at android.os.Parcel.readException(Parcel.java:2039)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:188)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
at android.content.ContentProviderProxy.call(ContentProviderNative.java:658)
at android.content.ContentResolver.call(ContentResolver.java:2042)
at android.provider.DocumentsContract.createDocument(DocumentsContract.java:1327)
at androidx.documentfile.provider.TreeDocumentFile.createFile(TreeDocumentFile.java:53)
at androidx.documentfile.provider.TreeDocumentFile.createFile(TreeDocumentFile.java:45)
Which doesn't make sense to me, since the tree should be writeable.
For what it's worth, the Uri I get back from Intent.ACTION_OPEN_DOCUMENT_TREE looks like this:
content://com.android.externalstorage.documents/tree/0B07-1910%3A
Interestingly, when I use that Uri to create a DocumentFile object to browse, using documentFile = DocumentFile.fromTreeUri(context, uri), then documentFile.getURI().toString() looks like:
content://com.android.externalstorage.documents/tree/0B07-1910%3A/document/0B07-1910%3A
i.e., it's had something appended to the end of it.
Then, I descend into what should be a writeable folder (like "Download"), and try creating a writeable file as described above. The "Download" folder gets the Uri:
content://com.android.externalstorage.documents/tree/0B07-1910%3A/document/0B07-1910%3ADownload
and the Uri I'm using for toPath, above, is then:
content://com.android.externalstorage.documents/tree/0B07-1910%3A/document/0B07-1910%3ADownload/Testing.tmp
which leads to the problems described previously trying to create it.
I haven't actually found any decent information about writing an arbitrary file under Storage Access Framework restrictions.
What am I doing wrong? Thanks. :)
Uri uri = uri obtained from ACTION_OPEN_DOCUMENT_TREE
String folderName = "questions.59189631";
DocumentFile documentDir = DocumentFile.fromTreeUri(context, uri);
DocumentFile folder = documentDir.createDirectory(folderName);
return folder.getUri();
Use createFile() for a writable file.

Get File Uri from Document Id in Storage Access Framework

I am using directory selection as described in this Google Sample. It does provide file name and mime type of the children of the selected directory. I can get Document ID of the file too, if I use COLUMN_DOCUMENT_ID on the Cursor Query.
I am interested in the file URI of the children instead. When I use ACTION_OPEN_DOCUMENT instead of ACTION_OPEN_DOCUMENT_TREE, I get the child uri easily which is just obtained from adding a %2Fchildfile.extention (%2F is just a forward slash). So I tried to get child file uri using the following code -
uri = Uri.parse(docUri.toString()+"%2F"+fileName);
I got the file name, however when I run exists() method on it (By converting it into DocumentFile), it returns false. That means, either I don't have the permission of the file or it's not the correct way to get children uri.
Am I missing something here or is there any other way I can select a folder and get file uri of all of it's children easily.
PS: I am currently checking it in Marshamallow.
After reading the doc and trying out certain examples, I got the following way to get a single file Uri from a selected docUri/treeUri
uri = DocumentsContract.buildDocumentUriUsingTree(docUri,docId);
And then you can convert it anytime into a DocumentFile using following code -
DocumentFile file = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (DocumentsContract.isDocumentUri(context, uri)) {
file = DocumentFile.fromSingleUri(context, uri);
} else {
file = DocumentFile.fromTreeUri(context, uri);
}
}
fromTreeUri() method is required for the selected Tree Directory, so that it can return true on file.exists() method call.
You need to remember that if the children contain any directory, then you can't call childDirectory.listFiles() on it. It'll give UnsupportedOperationException, because you don't have permission to access the child directory's file. Read more about this here.

java.io.IOException: Cannot make changes to file

I am using JAudioTagger library for reading and writing tags for an audio file. I am able to read the tags but unable to write them.
I am retrieving audio file path like this :
private String getSongPath(long songId) {
String path = null;
ContentResolver contentResolver = getContentResolver();
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = {MediaStore.Audio.Media.DATA};
String selection = MediaStore.Audio.Media._ID + " == ?";
String[] selectionArgs = {String.valueOf(songId)};
Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, null);
if (cursor != null) {
int pathCol = cursor.getColumnIndexOrThrow(projection[0]);
cursor.moveToFirst();
path = cursor.getString(pathCol);
cursor.close();
}
return path;
}
Then to write tags using JAudioTagger :
File songFile = new File(path); // path looks like /storage/3932-3434/Music/xyz.mp3
AudioFile audiofile = = AudioFileIO.read(songFile);
Tag tag = = audiofile.getTag();
tag.setField(FieldKey.TITLE, title);
// some more setField calls for different feilds
audiofile.commit();
The commit() method is giving following Exception :
org.jaudiotagger.audio.exceptions.CannotWriteException:
java.io.IOException: Cannot make changes to file xyz.mp3 at
org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:799) at
com.techapps.musicplayerplus.MainActivity$17.onClick(MainActivity.java:2125)
at
android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157)
at android.os.Handler.dispatchMessage(Handler.java:102) at
android.os.Looper.loop(Looper.java:148) at
android.app.ActivityThread.main(ActivityThread.java:5417) 06-18
10:59:48.134 8802-8802/com.techapps.musicplayerplus W/System.err:
at java.lang.reflect.Method.invoke(Native Method) at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) Caused
by: java.io.IOException: Cannot make changes to file Saibo.mp3 at
org.jaudiotagger.audio.mp3.MP3File.precheck(MP3File.java:824) at
org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:850) at
org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:783) at
org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:795)
I am running this code on Android 6 while my app is targeted at SDK 22. I have also mentioned following permission in manifest.
android.permission.WRITE_EXTERNAL_STORAGE
Still I am unable to write to SD card. Please help me. Thanks in advance.
You have to use Storage Access Framework (SAF) to access SD Card from API 19 (Kitkat) onward.
First we need to ask user to provide a URI of the folder we want to access. If we want access to entire SD card, user needs to provide URI of SD card's root folder.
For example, when user hits Edit button, we have to first show hint dialog box, asking user to select required directory in SD Card which we want to access. You can display following image in hint dialog box to ask user to select root directory of SD Card :
When user dismisses hint dialog box, you need to trigger Storage Access Framework :
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
// Get Uri from Storage Access Framework.
treeUri = resultData.getData();
pickedDir= DocumentFile.fromTreeUri(this, treeUri);
if (!isSDCardRootDirectoryUri(treeUri)) {
Toast.makeText(this, "Wrong directory selected. Please select SD Card root directory.", Toast.LENGTH_LONG).show();
createSDCardHintDialog().show();
return;
}
// Persist URI in shared preference so that you can use it later.
SharedPreferences sharedPreferences = getSharedPreferences(App.PREFERENCE_FILENAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(App.SDCARD_URI_KEY, treeUri.toString());
editor.apply();
// Persist access permissions, so you dont have to ask again
final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
private boolean isSDCardRootDirectoryUri(Uri treeUri) {
String uriString = treeUri.toString();
return uriString.endsWith("%3A");
}
Once you get Uri of user picked directory, you can perform write operation using SAF : (creadit : this answer )
public void writeFile(DocumentFile pickedDir) {
try {
DocumentFile file = pickedDir.createFile("image/jpeg", "try2.jpg");
OutputStream out = getContentResolver().openOutputStream(file.getUri());
try {
// write the image content
} finally {
out.close();
}
} catch (IOException e) {
throw new RuntimeException("Something went wrong : " + e.getMessage(), e);
}
}
It could be that you pointing to non existing file.
Check your path file by using Log.
Log.d("Activity", "path = " + path);
Android-M or API 23 introduced Runtime Permissions for reducing security flaws in android device.
To update your apps using Google Play services to handle Android 6.0 permissions, it’s good practice to manage the user’s expectations in setting permissions that the runtime may require. The following link will help you avoid potential issues.
https://developer.android.com/training/permissions/requesting.html
have you declared the permission
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ?
I saw that you already created an issue in the JAudioTagger GitHub repository which was advisable, but never got a universally working solution. My findings so far:
The answer mentioning SAF is correct, but it won't help you as SAF will provide a DocumentFile, not a File.
You might try to modify JAudioTagger to your needs, replacing File with DocumentFile, but the latter one has not all functions you will need.
Also InputStream and OutputStream will not help you, as JAudioTagger needs File and internally heavily uses RandomAccessFile which is not available either.
Google "forgot" to provide some getRandomAccessFileFromUri() which makes things even worse (Yes, there are hacks using Java reflection to work around this limitation...).
The "/proc/self/fd" method (How to handle SAF when I can only handle File or file-path?) will also not work immediately, as JAudioTagger needs copy and renaming functions that are not applicable to this kind of files. Particularly JAudioTagger will not find a suitable file name extension like ".m4a". Of course you could try to change JAudioTagger accordingly.
You might follow the advice to make a copy of the file to your personal storage, then apply JAudioTagger to it and finally copy it back to SD card, but:
If you want to use JAudioTagger to read from SD card, this will, as announced by Google, fail with Android 10. Starting with that version, you will not even have read access to the SD card via the File interface.
Further, the File interface gives you read access to SD cards with Android 9 and below, but not to other SAF devices, like USB OTG memory or SMB shares etc.
Of course you could also copy each file in order to read its metadata, but this will be awfully slow and is not suitable if you have more than a few files.
So my current advices are:
Try the "/proc/self/fd" method and modify JAudioTagger accordingly.
If the changes are too heavy, use the fd method for reading the tags and the copy method for writing.
BTW: I am currently modifying an older version of JAudioTagger for using both File and DocumentFile transparently, but the changes are tremendous, bear a high risk, need some help classes, and the work is unfinished, yet.
BTSW: The DocumentFile functions are painfully slow, compared to the File functions.

Categories

Resources