12

Background

After migrating to Android Q I can no longer find a suitable way to gain write access to the documents folder (/storage/emulated/0/Documents)

And before anyone mentions the many other questions on scoped storage I've read through many of them and from what I've seen so far all of the solutions use a app specific directory or accessing a media directory only (not documents folder access).

From what I understand in Android Q I can either choose:

  • Use a app specific directory that is only accessible by my app (or apps I give permission to)
  • Allow the user to select where they want the file stored (I think this can be a public location) ACTION_OPEN_DOCUMENT_TREE

Problem

The applications I develop are used to conduct tests on subjects, the data from the tests automatically gets stored in the publicly accessible Documents folder somewhat like: /storage/emulated/0/Documents/myAppName/subjectName/testData-todaysDate.pdf

When the user wants to access the test data they plug their smartphone into a computer and navigate to the documents folder and the rest is pretty obvious. For this reason I have to use publicly accessible storage. Also imagine they want to open the test data on their phone via another app, Same deal!

The solution I'm looking for

So the solution I'm looking for needs to be able to do the following:

  • Doesn't ask for permission multiple times (once is ok)
  • Automatically saves test data without user prompt (as hundreds of tests will be conducted)
  • Saves to public documents folder e.g. /storage/emulated/0/Documents/
  • Doesn't involve the user selecting the directory so NOT ACTION_OPEN_DOCUMENT_TREE
  • Ideally the data is persistent so uninstalling the app doesn't cause data lose

I get that these changes in Android Q are to bring users more "into the loop" with how apps are accessing their data but once they understand how your app is using your data there shouldn't be a problem.

blackapps
  • 8,011
  • 2
  • 11
  • 25
Jameson
  • 190
  • 1
  • 11
  • 4
    AFAIK, there is no solution meeting your requirements. If you switch from `Documents/` to `Downloads/`, you can [use `MediaStore.Downloads`](https://commonsware.com/blog/2020/01/11/scoped-storage-stories-diabolical-details-downloads.html). "but once they understand how your app is using your data there shouldn't be a problem" -- by that argument, you should not have a problem with `ACTION_OPEN_DOCUMENT_TREE`. The user needs to handle that exactly once, after which you can write as many documents into that tree as you like. – CommonsWare Feb 12 '20 at 00:26
  • 1
    With `ACTION_OPEN_DOCUMENT_TREE` can they select a public location when they setup the app for example and then we reuse that forever? That might be the best solution then, still dangerous as the user can choose a bad location but better then nothing. – Jameson Feb 12 '20 at 00:35
  • `When the user wants to access the test data they plug their smartphone into a computer and navigate to the documents folder`. Is this still possible with an Android Q device? Please report as i have no such device. – blackapps Feb 12 '20 at 09:26
  • You can let the MediaStore write your files to /storage/emulated/0/Documents and /storage/emulated/0/Download and /storage/emulated/0/Pictures and /storage/emulated/0/DCIM without any permission needed. – blackapps Feb 12 '20 at 09:42
  • 2
    "With `ACTION_OPEN_DOCUMENT_TREE` can they select a public location when they setup the app for example and then we reuse that forever?" -- yes. – CommonsWare Feb 12 '20 at 12:08
  • Check out https://stackoverflow.com/a/61886002/4685284 – Shahbaz Hashmi May 19 '20 at 08:47

2 Answers2

18

For Android Q only.

String collection = "content://media/1c11-2404/file"; // One can even write to micro SD card
String relative_path = "Documents/MyFolder";

Uri collectionUri = Uri.parse(collection);

ContentValues contentValues = new ContentValues();

contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
contentValues.put(MediaStore.MediaColumns.SIZE, filesize);
contentValues.put(MediaStore.MediaColumns.DATE_MODIFIED, modified );
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, relative_path);
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1)

Uri fileUri = context.getContentResolver().insert(collectionUri, contentValues);

Having an uri one can open an output stream to write the content for the file.

blackapps
  • 8,011
  • 2
  • 11
  • 25
  • 2
    @blackapps: OK, I can confirm that this works. For external storage, you would use `content://media/external/file` (also available via `MediaStore.Files.getContentUri("external")`). For removable storage, you would need the storage ID (`1c11-2404` in your code snippet). I am skeptical that this is intended to work, so I would not be shocked if there are problems with this technique in future versions of Android. But, this is an excellent find -- thanks for sharing it! – CommonsWare Feb 12 '20 at 22:46
  • Ok I'm slightly confused, before I was using `FileOutputStream` like so `outputStream = new FileOutputStream(filePath, false);` then I would simply `outputStream.write(dataToWrite.getBytes());` and then `.flush()` and `.close()` This only works with `File` of course, so I am not sure how you open an outputstream with a uri as you suggest? – Jameson Feb 12 '20 at 22:49
  • 2
    `OutputStream is = getContentResolver().openOutputStream(fileUri);`. – blackapps Feb 13 '20 at 00:14
  • 1
    Ok between @blackapps and @CommonsWare I was able to piece together enough to get something working. This is absolutely brilliant but as others have said I'm not sure how intended this was.Also worth noting `android:requestLegacyExternalStorage="true"` will persist (even if removed) unless you clear the data/cache on your app. – Jameson Feb 13 '20 at 01:12
  • regarding the UUID of an SD card, I found out it should be lowercase, contrary to what's returned from `StorageVolume` `getUuid()` – Thibault May 27 '20 at 04:22
0

try to put this line in your manifest file for android Q:

    android:requestLegacyExternalStorage="true"

in application tag.

Yu Hao
  • 119,891
  • 44
  • 235
  • 294
khushbu movaliya
  • 127
  • 1
  • 1
  • 12