I previously used external storage to store specific data that I would like to share between my applications (without having any contentprovider "host"), by using WRITE_EXTERNAL_STORAGE.
Not a media file, it is more like an encoded string in it.
It does not seem to be possible anymore on Android 11, without requesting MANAGE_EXTERNAL_STORAGE.
But this permission will not be granted by Google to all applications, and will require to fill a form, like everry "restricted permissions" (READ_CALL_LOG, READ_SMS, ACCESS_FINE_LOCATION, etc...) See support.google.com/googleplay/android-developer/answer/9888170
Exemple : By having XX applications, each one could be the first one to write a file (the first app used by the user basically), and the 3 other applications would read this file when started.
Any idea on how this can be achieved on Android 11? BlobManager seems to be appropriate but documentation is terrible (I tried it without success: new BlobStoreManager read write on Android 11)
private void writeFile(String data) {
try {
File f = new File(Environment.getExternalStorageDirectory(), FOLDER_NAME);
if (!f.exists()) {
boolean mkdirs = f.mkdirs();
if (!mkdirs) {
return;
}
}
File file = new File(f, FILE_NAME);
FileOutputStream outputStream = new FileOutputStream(file);
String encoded = Base64.encodeToString(data.getBytes(), Base64.DEFAULT);
outputStream.write(encoded.getBytes());
outputStream.close();
} catch (IOException e) {
Logger.e(TAG, "writeFile: IOException", e);
} catch (Exception e) {
Logger.e(TAG, "writeFile: Basic exception", e);
}
}
private String readFile() {
String data;
try {
File file = new File(Environment.getExternalStorageDirectory(), FOLDER_NAME + "/" + FILE_NAME);
if (!file.exists()) {
return "";
}
InputStream is = new FileInputStream(file);
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
String text = new String(buffer, Charset.forName("UTF-8"));
data = new String(Base64.decode(text, Base64.DEFAULT));
Logger.d(TAG, "readFile: decoded = " + data);
} catch (IOException e) {
Logger.e(TAG, "readFile: IOException", e);
return "";
} catch (IllegalArgumentException e) {
Logger.e(TAG, "readFile: Illegal Base64 import preset", e);
return "";
} catch (Exception e) {
Logger.e(TAG, "readFile: Basic exception", e);
return "";
}
return data;
}
EDIT: I tried some others solutions:
The External Public Storage way
An application "A" can write, and then read the file. But an other application "B" can not read the file written by "A"
I only get an access error: NotificationHelper - readFile: IOException java.io.FileNotFoundException: /storage/emulated/0/Download/myfolder/settings.bin: open failed: EACCES (Permission denied) at libcore.io.IoBridge.open(IoBridge.java:492) at java.io.FileInputStream.(FileInputStream.java:160)
file = new File (Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), FOLDER_NAME + "/" + FILE_NAME);
The mediastore way
But just with one app, I have issues: The app can not override a file writtend earlier, it creates multiple instance "my_file", "myfile(1), ..." And I have error when trying to read it:
java.io.FileNotFoundException: open failed: ENOENT (No such file or directory) at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:151) at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:781) at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1986) at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1801) at android.content.ContentResolver.openInputStream(ContentResolver.java:1478) at fr.gg.frameworkmobile.utils.NotificationHelper.readFile(NotificationHelper.java:388)
private void writeFile(String data) {
String outputFilename = "my_file";
String outputDirectory = "my_sub_directory"; // The folder within the Downloads folder, because we use `DIRECTORY_DOWNLOADS`
ContentResolver resolver = AbstractMobileApplication.getInstance().getApplicationContext().getContentResolver();
ContentValues values = new ContentValues();
// save to a folder
values.put(MediaStore.Files.FileColumns.DISPLAY_NAME, outputFilename);
values.put(MediaStore.Files.FileColumns.MIME_TYPE, "application/my-custom-type");
values.put(MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + outputDirectory);
values.put(MediaStore.Files.FileColumns.IS_PENDING, 1);
Uri uri = resolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), values);
// You can use this outputStream to write whatever file you want:
OutputStream outputStream = null;
Log.d(TAG, "writeFile: >>>>>>>>" + uri.getPath());
try {
outputStream = resolver.openOutputStream(uri);
String encoded = Base64.encodeToString(data.getBytes(), Base64.DEFAULT);
outputStream.write(encoded.getBytes());
outputStream.close();
values.clear();
values.put(MediaStore.Files.FileColumns.IS_PENDING, 0);
resolver.update(uri, values, null, null);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private String readFile() {
String data;
String outputFilename = "my_file";
String outputDirectory = "my_sub_directory"; // The folder within the Downloads folder, because we use `DIRECTORY_DOWNLOADS`
ContentResolver resolver = AbstractMobileApplication.getInstance().getApplicationContext().getContentResolver();
ContentValues values = new ContentValues();
// save to a folder
values.put(MediaStore.Files.FileColumns.DISPLAY_NAME, outputFilename);
values.put(MediaStore.Files.FileColumns.MIME_TYPE, "application/my-custom-type");
values.put(MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + outputDirectory);
Uri uri = resolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), values);
// You can use this outputStream to write whatever file you want:
Log.d(TAG, "readFile: >>>>>>>>" + uri.getPath());
try {
InputStream is = resolver.openInputStream(uri);
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
String text = new String(buffer, Charset.forName("UTF-8"));
data = new String(Base64.decode(text, Base64.DEFAULT));
Logger.d(TAG, "readFile: decoded = " + data);
} catch (FileNotFoundException e) {
data = "";
e.printStackTrace();
} catch (IOException e) {
data = "";
e.printStackTrace();
}
return data;
}
Content Providers is not a solution either because none of the apps is a "host"
They all are the host. You will need to maintain N copies of the data, one per app, with some sort of coordination mechanism between them for handling modifications to that data. You already needed a coordination mechanism, if multiple of the apps might modify your common file in your old solution.
For example, if the data changes infrequently:
The first app in your suite, when first run, sends a secured "can I get a copy of the data?" broadcast, which nobody responds to, since it is the first app in your suite
The first app sets up the data
Subsequent apps, when first run, send a the same "can I get a copy of the data?" broadcast
Each app has a receiver for the broadcast, and if they have the data, sends a "here is a copy of the data" broadcast in reply, which either has the data itself (if it is small) or has a Uri
to a ContentProvider
that can supply the data. Ideally, the data has a timestamp or some other versioning information in it.
Each app, if it modifies the data, sends that "here is a copy of the data" broadcast.
Each app has a receiver for the "here is a copy of the data" broadcast and uses that to grab the data if it is newer than what they have (or grabs it for the first time if they do not already have the data).
This is complex, with risks of collisions if two apps try modifying the data around the same time.
You could consider an election protocol and have a single app be the "owner" of the data, with the other apps just having backup copies, and with a new election if the current owner app is uninstalled. Done properly, this could reduce the risks of collisions, at the cost of even more complexity.
The simple solution is to allow the user to specify where this shared content resides, via ACTION_CREATE_DOCUMENT
(for your first app) and ACTION_OPEN_DOCUMENT
(for subsequent apps). However, you rejected this ("And it requires to be "invisible" for the users: no file picker"). My recommendation would be for you to relax this requirement. And, you still need some coordination mechanism, if multiple of the apps might modify your common content, just as you did with the common file approach you took originally.
And, you could always consider eliminating the suite, merging the functionality into a single app. Or, adopt more of a "host-and-plugins" model for the suite, such that each plugin app does not need independent access to the data.