0

The question is specifically for apps targeting API 31 and above.

I have referred to a lot of similar StackOverflow questions, Official Docs, etc.
There are some limitations to API 31 as mentioned here - Docs.

Usecase

To write a JSON file to the device's external storage so that the data is not lost even if the app is uninstalled.

How to achieve this for apps, targeting API 31 and above?

My current code saves the file to app-specific storage directory obtained using context.getExternalFilesDir(null).

The problem is that the file is not retained when the app is uninstalled.

Any blogs or codelabs explaining the exact scenario are also fine.

Note

  1. Aware that the user can see, modify or delete the file from other apps when we store it in the external storage directory - That is fine.
  2. Device root directory, Documents, or any other location is fine, given that all devices can access the file in the same way.
Abhimanyu
  • 11,351
  • 7
  • 51
  • 121
  • Use the Storage Access Framework (e.g., `ACTION_CREATE_DOCUMENT` / `ActivityResultContracts.CreateDocument` or `ACTION_OPEN_DOCUMENT_TREE` / `ActivityResultContracts.OpenDocumentTree`), to let the user tell you where on the user's device the user would like you to store the user's content. – CommonsWare Apr 03 '22 at 12:26
  • I see some limitations on that here - https://developer.android.com/training/data-storage/shared/documents-files#document-tree-access-restrictions – Abhimanyu Apr 03 '22 at 12:28
  • On Android 11 (API level 30) and higher, you cannot use the ACTION_OPEN_DOCUMENT_TREE intent action to request access to the following directories - `Root` and `Download` directory. – Abhimanyu Apr 03 '22 at 12:29
  • @CommonsWare, From the docs, my understanding is that for any location other than app-specific directories, the user has to manually select the file or directory for the app using the `ACTION_CREATE_DOCUMENT` or `ACTION_OPEN_DOCUMENT_TREE`. There is no way to skip the user's manual location selection? – Abhimanyu Apr 03 '22 at 12:33
  • Correct. If you want to store non-media content on the device indefinitely, the user needs to be involved in choosing the location. – CommonsWare Apr 03 '22 at 12:36
  • API30+ open and save /documents accounting community files is a hazzel to me. Unfortunately I have to update my app and API30 is minimum acceptance for the app store. The app is a bookkeeping file browser of industry-specific standard accounting format files (CP450 text, bytearray), in Android terms a document? I used to used NDK fopen() after picker got the name, don't work, but bit-stream java do. To me it looks like the NDK is not synchronized with the SDK? And for saving the picker just rejects the intent.resolveActivityInfo(activity.getPackageManager(), 0) call for the /documents folder. – Jan Bergström Oct 31 '22 at 17:43

3 Answers3

1

Your app can store as much files in public Documents or Download directory and sub directories as it wants without any user interaction. All with classic file means.

At reinstall you use ACTION_OPEN_DOCUMENT_TREE to let the user choose the used subfolder.

blackapps
  • 8,011
  • 2
  • 11
  • 25
1

Thanks to @CommonsWare's comments on my question, I got how to implement it.

My use-case is to create, read and write a JSON file.

I am using Jetpack Compose, so the code shared is for Compose.

Composable code,

val JSON_MIMETYPE = "application/json"

val createDocument = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.CreateDocument(
        mimeType = JSON_MIMETYPE,
    ),
) { uri ->
    uri?.let {
        viewModel.backupDataToDocument(
            uri = it,
        )
    }
}
val openDocument = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.OpenDocument(),
) { uri ->
    uri?.let {
        viewModel.restoreDataFromDocument(
            uri = it,
        )
    }
}

ViewModel code,

fun backupDataToDocument(
    uri: Uri,
) {
    viewModelScope.launch(
        context = Dispatchers.IO,
    ) {
        // Create a "databaseBackupData" custom modal class to write data to the JSON file.
        jsonUtil.writeDatabaseBackupDataToFile(
            uri = uri,
            databaseBackupData = it,
        )
    }
}

fun restoreDataFromDocument(
    uri: Uri,
) {
    viewModelScope.launch(
        context = Dispatchers.IO,
    ) {
        val databaseBackupData = jsonUtil.readDatabaseBackupDataFromFile(
            uri = uri,
        )
        // Use the fetched data as required
    }
}

JsonUtil

private val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()
private val databaseBackupDataJsonAdapter: JsonAdapter<DatabaseBackupData> = moshi.adapter(DatabaseBackupData::class.java)

class JsonUtil @Inject constructor(
    @ApplicationContext private val context: Context,
) {
    fun readDatabaseBackupDataFromFile(
        uri: Uri,
    ): DatabaseBackupData? {
        val contentResolver = context.contentResolver

        val stringBuilder = StringBuilder()
        contentResolver.openInputStream(uri)?.use { inputStream ->
            BufferedReader(InputStreamReader(inputStream)).use { bufferedReader ->
                var line: String? = bufferedReader.readLine()
                while (line != null) {
                    stringBuilder.append(line)
                    line = bufferedReader.readLine()
                }
            }
        }
        return databaseBackupDataJsonAdapter.fromJson(stringBuilder.toString())
    }

    fun writeDatabaseBackupDataToFile(
        uri: Uri,
        databaseBackupData: DatabaseBackupData,
    ) {
        val jsonString = databaseBackupDataJsonAdapter.toJson(databaseBackupData)
        writeJsonToFile(
            uri = uri,
            jsonString = jsonString,
        )
    }

    private fun writeJsonToFile(
        uri: Uri,
        jsonString: String,
    ) {
        val contentResolver = context.contentResolver
        try {
            contentResolver.openFileDescriptor(uri, "w")?.use {
                FileOutputStream(it.fileDescriptor).use { fileOutputStream ->
                    fileOutputStream.write(jsonString.toByteArray())
                }
            }
        } catch (fileNotFoundException: FileNotFoundException) {
            fileNotFoundException.printStackTrace()
        } catch (ioException: IOException) {
            ioException.printStackTrace()
        }
    }
}
Abhimanyu
  • 11,351
  • 7
  • 51
  • 121
0

First you ask is there a way to skip user manual selection, the answer is yes and no, it's yes if you get MANAGE_EXTERNAL_STORAGE permission from the user but as it is a sensitive and important permission google will reject your app to be published on google play if it is not a file-manager or antivirus or other app that really needs this permission. read this page for more info and the answer is no if you don't use MANAGE_EXTERNAL_STORAGE permission.

You should use Storage Access Framework, It doesn't need any permission, I will tell you how to implement it:

Imagine you have saved JSON to a text file, here is the steps to restore data from it:

1- We need a method that displays a dialog to the user, so he/she can choose saved file:

private void openFile() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("text/*");
    someActivityResultLauncher.launch(intent);
}

2- We should implement onActivityResult to get the uri of the file that user has selected, so add someActivityResultLauncher to activity class:

    ActivityResultLauncher<Intent> someActivityResultLauncher;

3- Add following methods to Activity class:

 private void registerActivityResult()
{
    someActivityResultLauncher = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            new ActivityResultCallback<ActivityResult>() {
                @Override
                public void onActivityResult(ActivityResult result) {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        // There are no request codes
                        Intent data = result.getData();
                        Uri uri = data.getData();
                            // Perform operations on the document using its URI.
                            try {
                                String txt = readTextFromUri(Setting_Activity.this, uri);
                                dorestore(txt);
                            }catch (Exception e){}

                    }
                }
            });
}

4- Call the above method in onCreate() method of activity:

@Override
protected void onCreate(Bundle savedInstanceState) {
    //......   
    registerActivityResult();
    //.....
 }

5- Implement the readTextFromUri() method we used in step 3:

public static String readTextFromUri(Context context, Uri uri) throws IOException {
    StringBuilder stringBuilder = new StringBuilder();
    try (InputStream inputStream =
                 context.getContentResolver().openInputStream(uri);
         BufferedReader reader = new BufferedReader(
                 new InputStreamReader(Objects.requireNonNull(inputStream)))) {
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
        }
    }
    return stringBuilder.toString();
}

That's all, just implement the dorestore() method we used in step 3 to restore your json data. in mycase it looke something like this:

private void dorestore(String data)
{
    ArrayList<dbObject_user> messages = new ArrayList<>();
    try {
           JSONArray jsonArray = new JSONArray(data);
           //....
           //parsing json and saving it to app db....
           //....
     }
    catch (Exception e)
    {}
  }

just call the openFile() in step 1. That's all.

farhad.kargaran
  • 2,233
  • 1
  • 24
  • 30