I have a DocumentsProvider that simply republishes a directory from an external file storage. Everything works well, however when deleting a file from the Android Files application, the directory does not get refreshed after the file is deleted. I'm probably supposed to call contentResolver.notifyChange, but I'm struggling to find the proper content uri.
#Throws(FileNotFoundException::class)
override fun deleteDocument(documentId: String) {
val file = getFileForDocId(documentId)
if (!file.delete()) {
throw FileNotFoundException("Failed to delete document with id $documentId")
}
// Notify parent about directory change
file.parentFile?.let { parent ->
val parentDocId = getDocIdForFile(parent)
val updatedUri = DocumentsContract.buildChildDocumentsUri(
BuildConfig.FILES_AUTHORITY, parentDocId)
context!!.contentResolver.notifyChange(updatedUri, null)
}
}
I tried many combinations with the Uri, but it still doesn't work. I even tried to make all the identifiers in URL lowercase, no luck. The only thing that seemed to have at least some effect was trying to refresh the root - it forced queryRoots.
Deleting the file on the Google Drive seems to trigger the proper refresh, so there is probably some way.
Ultimately this is the answer: https://stackoverflow.com/a/27583807/149901
When returning a Cursor from the query methods, the setNotificationUri method must be used with the proper uri. Then the notifyChange works correctly.
Related
Setup: I get an URI of the directory in shared storage using procedure described here: Access documents and other files from shared storage / Grant access to a directory's contents.
Question: How to create a file inside this directory?
More details:
The guide mentioned above explains, how to list files in this directory, open some file for reading or modification and also how to delete it. All this using ContentResolver. (Edit: actually guide does not explain it also, but just how to delete or modify files obtained interactively with ACTION_OPEN_DOCUMENT). But I cannot see, how to create some file inside this directory.
Two candidates, which I can imagine, could help me with the job are: ContentResolver.insert() or ParcelFileDescriptor(file, MODE_CREATE), but I cannot figure out how I could use them in this situation.
I should also mention, that I know about the possibility to create the file, when user interactively selects the file name and location, but it is not good solution for me. In my context I need to create several files inside one folder. All files should be accessible/modifiable/deletable by user from outside of my application, thus "shared storage". And the files are named and located within their "main" directory according to some conventions, so I don't want the user to explicitly provide me the name for each file, but just the location of "main directory".
Point 1: blackapps is right in the comments to the original question and this question is duplicate of e.g. those:
Create new file in the directory returned by Intent.ACTION_OPEN_DOCUMENT_TREE
or
Write file to directory using Intent.ACTION_OPEN_DOCUMENT_TREE
Point 2 (Quick answer): One should use in this case DocumentFile.fromTreeUri(this, uri) with the URI returned from the Intent.ACTION_OPEN_DOCUMENT_TREE. This worked for me out of the box.
Point 3: One thing was still confusing for me:
If I tried to use something like this (from the original guide, which I used) - to query metadata of the URI:
Cursor cursor = getContentResolver().query(uri, null, null, null, null, null);
Or if I tried to call something like this, hoping to create new document without use of DocumentFile, but instead using directly DocumentsContract:
Uri createdDocumentUri = DocumentsContract.createDocument(
getContentResolver(),
uri,
"text/plain",
"someDocument.txt");
Then I always got an exception. In the first case it was:
java.lang.UnsupportedOperationException: Unsupported Uri content://com.android.externalstorage.documents...
And in the second case:
java.lang.IllegalArgumentException: Invalid URI: content://com.android.externalstorage.documents...
Different exceptions, but in both cases the message is "Unupported URI".
For a while I could not understand, why is it the case. But when I looked inside DocumentFile.fromTreeUri() and discovered, that instead of using URI returned by Intent.ACTION_OPEN_DOCUMENT_TREE directly, one needs to process it in a way like this (the code is taken from DocumentFile.fromTreeUri()):
String documentId = DocumentsContract.getTreeDocumentId(uri);
Uri newUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
The documentation of DocumentsContract.buildDocumentUriUsingTree(), explains little bit, what is going on:
... However, instead of directly accessing the target document, the
returned URI will leverage access granted through a subtree URI,
typically returned by Intent#ACTION_OPEN_DOCUMENT_TREE. The target
document must be a descendant (child, grandchild, etc) of the subtree.
This is typically used to access documents under a user-selected
directory tree, since it doesn't require the user to separately
confirm each new document access.
I hope, that this answer will help someone to save a bit of confusion, which I had, while I was trying to clarify this topic...
I'm surprised when delete folder from gallery and getting that folder by programatically it's returning isExists() = true.
if(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), directory).exists()) {
return true
}
Note: However it's happening mostly customised devices of android, is there any way to find directory is exists or not?
When you call File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), directory) you are actually creating folder and then you are checking for exist of that file, so you were always get true. Lookout documentation for constructor of File(File parent, String child).
Creates a new instance from a parent abstract
better to use concet string and get file like File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath()+ directory) to check exist or not.
As Google is trying to enforcing apps to use SAF for storage access, I am trying to adapt my app to use SAF replacing java file io apis.
I have spent many hours study the SAF APIs (mainly DocumentFile and DocumentContract classes) but still have some difficulties.
First one is how to move a file to another directory? DocumentFile does have a method to rename a file, but it is just the display name of the file. How can I move a file to another folder, if it is a huge file which I don't want to copy it. Assume src and dst are on the same partition.
Second question is how to list child files efficiently. I checked the source code and found that DocumentFile.listFiles() impl only queries the child files with single projection [ID]. And later when I want to display the files in a list view with their names, the call to DocumentFile.getName() will trigger another query via content resolver for each file again. This is a huge impact on the performance of the code. Especially when I try to sort an array of DocumentFile by their names, 30+ files will cost 600+ms, which is far beyond the acceptable. I doubt whether I am using the correct API set. Could anyone point out a better way to list files with names (and other properties)?
Simple Storage is a library that simplify SAF across API levels. Suppose that you want to move a MP4 file from directory Video in external storage into directory Others in SD card. Let's assume that AAAA-BBBB as SD card's storage ID:
val source = DocumentFileCompat.fromSimplePath(context, basePath = "Video/Infinity War.mp4")
val targetFolder = DocumentFileCompat.fromSimplePath(context, storageId = "AAAA-BBBB", basePath = "Others")
// To move file:
source.moveFileTo(context, targetFolder, callback)
// To copy file:
source.copyFileTo(context, targetFolder, callback)
I'm working on a prototype app that would read and display data from a large repository of JSON files. I'm currently wondering about the best way to model this behavior in the pubspec.yaml and the in the flutter code itself.
Right now I'm initially loading a catalog file in JSON, this creates a scrollable listview of catalog entries, each of those has its own JSON file in a subdirectory (relative to the catalog file). Do I need to add each new file to the pubspec.yaml, or can I just directly access them.
You could also just access files in the file system without specifying the files in the pubspec.yaml.
Im not sure, if the syntax is still correct, because it is an old project I worked on but it should work in a similar way.
File _themeFile;
Future<Null> _loadThemeFile() async {
String dir = (await PathProvider.getApplicationDocumentsDirectory()).path;
_themeFile = new File('$dir/theme.txt');
if (!await _themeFile.exists()) {
_themeFile.create();
await _themeFile.writeAsString(JSON.encode({
'theme': 'dark',
'colorIndex': 0,
'displayDone': true,
}));
}
}
This snippet looks for the file with the name theme.txt and returns it. If it does not exist, it creates the file. You wouldn't probably need this exactly like that, but I think you can use it as a starting point.
If I call File.delete() are the effects on the underlying file system immediately visible? Can I write to the same file name in the same process/thread after without worrying about bad things happening? If not, is there a way to sync the underlying file system with just a File object?
File.delete() return a boolean telling you if the file has been correctly deleted.
So you could write something like :
if(yourFile.delete()) {
//keep doing what you want. You are now sure file has been deleted !
}
Also, before writing a new file, you could check if a file with the same name already exists.
From Oracle documentation :
Returns:
true if and only if the file or directory is successfully deleted; false otherwise
Oracle source
Also there is a SO thread that might help you