Search code examples
androidlocal-storagestorageandroid-12

Store files in android app (targeting API 31 and above) which is retained even after the app is uninstalled


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.

Solution

  • 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()
            }
        }
    }