简体   繁体   中英

Alternative for MediaScanner scanFile / CameraRoll on Android 29/Q

Google Play Store set new requirements regading Android Scoped Storage regarding apps using the manifest flag requestLegacyExternalStorage .
My app is using CameraRoll package from React Native community which does not yet support the scoped storage (and require the requestLegacyExternalStorage flag to work) and the timeline is very short ( May 5th 2021 ). Is there any alternative for CameraRoll? The goal here is to show the image in the user Gallery apps, like Google photos or vendor default Gallery without extra actions on the user side.

Original Google Play message:

 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.

Digging into ReactNative CameraRoll package, it does much more than simply just scanning the file for the OS to shows up in user Gallery apps. The solution here as some repercusion:

  1. The image need to be in a public directory for it to show up (= read access) in any Gallery apps, so not within the App external storage:

    • NOT: storage/android/data/com.example/Pictures
    • either in Pictures or DCIM or Download (check Environment.DIRECTORY_DCIM siblings)
  2. Need a native package for android

The code:

React Native part:

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

Native package (don't forget to replace the APPNAME by your app folder)

    @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 https://stackoverflow.com/a/64359655/1377145
    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;
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM