将文件存储在 android 应用程序(针对 API 31 及以上),即使在卸载应用程序后仍保留

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

此问题专门针对 API 31 岁及以上的应用。

我参考了很多类似的Whosebug问题,官方文档等
API 31 有一些限制,如此处所述 - Docs.

用例

写入JSON文件到设备的外接存储,这样即使卸载应用程序也不会丢失数据。

如何针对 API 31 岁及以上的应用实现这一目标?

我当前的代码将文件保存到使用 context.getExternalFilesDir(null).

获得的特定于应用程序的存储目录

问题是卸载应用后文件没有保留

解释确切场景的任何博客或代码实验室也很好。

备注

  1. 意识到当我们将文件存储在外部存储目录中时,用户可以从其他应用程序中查看、修改或删除该文件 - 这很好。
  2. 设备根目录、文档或任何其他位置都可以,因为所有设备都可以以相同的方式访问该文件。

您的应用程序可以在 public 文档或下载目录和子目录中存储任意数量的文件,无需任何用户交互。全部采用经典文件方式。

重新安装时使用 ACTION_OPEN_DOCUMENT_TREE 让用户选择使用的子文件夹。

首先你问有没有一种方法可以跳过用户手动选择,答案是有也没有,如果你得到用户的 MANAGE_EXTERNAL_STORAGE 许可就可以,但是因为它是一个敏感且重要的许可 google 将拒绝您的应用在 google play 上发布,如果它不是真正需要此权限的 file-manager 或防病毒或其他应用。 read this page for more info 如果您不使用 MANAGE_EXTERNAL_STORAGE 权限,答案是否定的。

你应该使用存储访问框架,它不需要任何权限,我会告诉你如何实现它:

假设您已将 JSON 保存到文本文件,以下是从中恢复数据的步骤:

1- 我们需要一个向用户显示对话框的方法,因此 he/she 可以选择保存的文件:

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

2- 我们应该实现 onActivityResult 以获取用户选择的文件的 uri,因此将一些 ActivityResultLauncher 添加到 activity class:

    ActivityResultLauncher<Intent> someActivityResultLauncher;

3- 添加以下方法到 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-在activity的onCreate()方法中调用上述方法:

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

5- 实现我们在步骤 3 中使用的 readTextFromUri() 方法:

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

就是这样,只需实现我们在步骤 3 中使用的 dorestore() 方法即可恢复您的 json 数据。 在我的情况下,它看起来像这样:

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)
    {}
  }

只需在步骤 1 中调用 openFile() 即可。

感谢@CommonsWare 对我的问题的评论,我知道了如何实现它。

我的use-case是创建、读取和写入一个JSON文件。

我正在使用 Jetpack Compose,所以共享的代码是针对 Compose 的。

可组合代码,

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代码,

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