Android 29/Q 媒体扫描仪扫描文件/相机胶卷的替代方案

Alternative for MediaScanner scanFile / CameraRoll on Android 29/Q

Google Play 商店设置新要求 Android 关于使用清单标志 requestLegacyExternalStorage.
的应用程序的分区存储 我的应用程序正在使用 CameraRoll package from React Native community,它尚不支持分区存储(并且需要 requestLegacyExternalStorage 标志才能工作)并且时间线非常短(2021 年 5 月 5 日 ). CameraRoll 有其他选择吗? 此处的目标是在用户图库应用程序中显示图像,例如 Google 照片或供应商默认图库,而无需在用户端执行额外操作。

原Google播放留言:

    Starting May 5th, you must let us know why your app requires broad storage access APPNAME 14 avr. 2021 19:26

    We've detected that your app contains the requestLegacyExternalStorage flag in the manifest file of 1 or more of your app bundles or APKs.

    Developers with apps on devices running Android 11+ must use Scoped Storage to give users better access control over their device storage. To release your app on Android 11 or newer after May 5th, you must either:

    Update your app to use more privacy friendly best practices, such as the Storage Access Framework or Media Store API
    Update your app to declare the All files access (MANAGE_EXTERNAL_STORAGE) permission in the manifest file, and complete the All files access permission declaration in Play Console from May 5th
    Remove the All files access permission from your app entirely

    For apps targeting Android 11, the requestLegacyExternalStorage flag will be ignored. You must use the All files access permission to retain broad access.

    Apps requesting access to the All files access permission without a permitted use will be removed from Google Play, and you won't be able to publish updates.

深入研究 ReactNative CameraRoll 包,它所做的不仅仅是扫描 OS 文件以显示在用户图库应用程序中。 这里的解决方案作为一些反响:

  1. 图像需要位于 public 目录中才能在任何图库应用程序中显示(=读取权限),而不是在应用程序外部存储中:

    • 不是:storage/android/data/com.example/Pictures
    • PicturesDCIMDownload 中(检查 Environment.DIRECTORY_DCIM 兄弟姐妹)
  2. 需要 android

    的原生包

代码:

React Native部分:

import RNFS from 'react-native-fs'
const { PNModule } = ReactNative.NativeModules

try {
    if (Platform.OS === 'android' && Platform.Version >= 29) {
        // Google ask that the requestLegacyExternalStorage is no longer used when targeting android 11, and use
        // the scoped storage or the new global permission, see https://gitlab.inria.fr/floristic/pn-mobile-test/-/issues/417
        // Solution here, custom module which use the MediaStore API and copy the file to the DCIM folders.
        const segments = path.split('/')
        const fileName = segments[segments.length - 1]

        const fileUriPath = await PNModule.moveToMediaStore(path.replace('file://', ''), fileName)
        if (!fileUriPath) {
            return null
        }
        const scanResult = await RNFS.scanFile(fileUriPath)
        if (fileUriPath.startsWith('file:///')) {
            return fileUriPath
        }
        return `file://${fileUriPath}`
    }
    return await CameraRoll.save(path)
} catch (error) {
    console.error(error)
}

原生包 (不要忘记将 APPNAME 替换为您的应用程序文件夹)

    @ReactMethod
    public void moveToMediaStore(String filePath, String fileName, Promise promise) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            promise.resolve(null);
            return;
        }
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");

        values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/APPNAME");
        values.put(MediaStore.MediaColumns.IS_PENDING, 1);

        ContentResolver resolver = getReactApplicationContext().getContentResolver();
        Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

        try {
            OutputStream fos = resolver.openOutputStream(imageUri);
            copy(new File(filePath), fos);
            values.clear();
            values.put(MediaStore.Video.Media.IS_PENDING, 0);
            resolver.update(imageUri, values, null, null);
            promise.resolve(getNameFromContentUri(getReactApplicationContext(), imageUri));
        } catch (Exception e) {
            e.printStackTrace();
            promise.reject(e);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    public static void copy(File src, OutputStream out) throws IOException {
        try (InputStream in = new FileInputStream(src)) {
            FileUtils.copy(in, out);
        }
    }

    // From 
    public static String getNameFromContentUri(Context context, Uri contentUri){
        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(contentUri, null, null, null, null);
        cursor.moveToFirst();
        String document_id = cursor.getString(0);
        document_id = document_id.substring(document_id.lastIndexOf(":") + 1);
        cursor.close();

        cursor = contentResolver.query(
            android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            null, MediaStore.Images.Media._ID + " = ? ", new String[]{document_id}, null);
        cursor.moveToFirst();
        String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
        cursor.close();
        return path;
    }