在 Android 11 上使用外部存储写入多个应用程序使用的文件

Use external storage to write file used by multiple applications on Android 11

我以前使用外部存储来存储我想在我的应用程序之间共享的特定数据(没有任何内容提供者“主机”),方法是 WRITE_EXTERNAL_STORAGE。

不是媒体文件,它更像是其中的编码字符串。

在 Android 11 上似乎不可能再不请求 MANAGE_EXTERNAL_STORAGE。

但此权限不会被 Google 授予所有应用程序,并且需要填写表格,就像每个“受限权限”(READ_CALL_LOG、READ_SMS、ACCESS_FINE_LOCATION, 等等...) 参见 support.google.com/googleplay/android-developer/answer/9888170

例子: 通过拥有XX个应用程序,每个应用程序都可以第一个写入文件(基本上是用户使用的第一个应用程序),其他3个应用程序将在启动时读取该文件。

知道如何在 Android 11 上实现吗? BlobManager 似乎是合适的,但文档很糟糕(我试过但没有成功: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;
}

编辑: 我尝试了其他一些解决方案:

外部Public存储方式

应用程序“A”可以写入,然后读取文件。但是另一个应用程序“B”无法读取“A”写入的文件

我只收到一个访问错误: NotificationHelper - 读取文件:IOException java.io.FileNotFoundException:/storage/emulated/0/Download/myfolder/settings.bin:打开失败:EACCES(权限被拒绝) 在 libcore.io.IoBridge.open(IoBridge.java:492) 在 java.io.FileInputStream.(FileInputStream.java:160)

file = new File (Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), FOLDER_NAME + "/" + FILE_NAME);

媒体存储方式

但是只有一个应用程序,我遇到了问题:该应用程序无法覆盖之前编写的文件,它创建了多个实例“my_file”、“myfile(1)”、... 我在尝试阅读时遇到错误:

java.io.FileNotFoundException:打开失败:ENOENT(没有那个文件或目录) 在 android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:151) 在 android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:781) 在 android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1986) 在 android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1801) 在 android.content.ContentResolver.openInputStream(ContentResolver.java:1478) 在 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;
}

将您的文件写入 public 目录 DCIM 或图片。

或这些目录中的子文件夹。

文件的扩展名应为 .jpg 或 .png。

您使用了 .dat,您可以制作 .dat.jpg 来识别这些文件。

Content Providers is not a solution either because none of the apps is a "host"

他们都是主持人。您将需要维护 N 个数据副本,每个应用程序一个,它们之间有某种协调机制来处理对该数据的修改。如果多个应用程序可能会修改您旧解决方案中的公用文件,您已经需要一个协调机制。

例如,如果数据变化不频繁:

  • 您套件中的第一个应用程序,当第一个 运行 时,会发送一个安全的“我可以获取数据副本吗?”广播,没有人响应,因为它是您套件中的第一个应用程序

  • 第一个应用设置数据

  • 后续应用程序,当第一个 运行 时,发送相同的“我可以获取数据副本吗?”广播

  • 每个应用程序都有一个广播接收器,如果它们有数据,则发送“这里是数据的副本”广播作为回复,它本身有数据(如果它很小)或者有一个 Uri 到一个 ContentProvider 可以提供数据。理想情况下,数据中包含时间戳或其他一些版本控制信息。

  • 每个应用程序,如果它修改了数据,发送“这里是数据的副本”广播。

  • 每个应用程序都有一个接收“这里是数据的副本”广播的接收器,如果数据比他们拥有的更新(或第一次获取数据),则使用它来获取数据如果他们还没有数据)。

这很复杂,如果两个应用大约同时尝试修改数据,则存在发生冲突的风险。

您可以考虑 election protocol 并让一个应用程序成为数据的“所有者”,而其他应用程序只有备份副本,如果当前所有者应用程序被卸载,则进行新的选择。如果处理得当,这可以降低碰撞风险,但代价是更加复杂。

简单的解决方案是允许用户通过 ACTION_CREATE_DOCUMENT(对于您的第一个应用程序)和 ACTION_OPEN_DOCUMENT(对于后续应用程序)指定此共享内容所在的位置。但是,您拒绝了这一点(“并且它需要对用户“不可见”:没有文件选择器”)。我的建议是放宽此要求。而且,您仍然需要一些协调机制,如果多个应用程序可能会修改您的公共内容,就像您最初采用的公共文件方法一样。

而且,您始终可以考虑取消套件,将功能合并到一个应用程序中。或者,为套件采用更多的“主机和插件”模型,这样每个插件应用程序就不需要独立访问数据。