Google photo and video permissions solution

Background: Google’s new policy

In October 2023, Google introduced a new policy on photo and video permissions, requiring developers to adjust their app permissions by August 31, 2024.

Timeline

  • October 2023: New photo and video permissions policy announced.
  • August 31, 2024:
    • If your app uses photos or videos infrequently, remove READ_MEDIA_IMAGES and READ_MEDIA_VIDEO permissions from the app manifest and switch to the system photo picker if needed.
    • Request an extension until January 2025 if you need more time to revoke permissions or switch to the picker.
    • If your app has core or broad usage scenarios, use the declaration form in Google Play Console to justify the need for these permissions.

Solution

Scenario 1: High-frequency multimedia usage in IM apps

Your App is a high-frequency multimedia app. If your app has core or broad usage scenarios and frequently uses multimedia functionality, retain these permissions by submitting a declaration form in Google Play Console.

Steps to retain permissions
  1. Access Google Play Console: Log in and locate your app.
  2. Fill out the declaration form: Provide detailed justification for high-frequency multimedia usage.
  3. Submit for review: Wait for Google’s review results.

Learn more about Google Play’s photo and video permissions policy

When publishing your app, refer to this video for permission explanations (VPN required): Permission explanation video

Scenario 2: Low-frequency multimedia usage in IM apps (or if Google rejects your request)

Step 1: Remove multimedia permissions

Remove these permissions from your app’s AndroidManifest.xml using the tools:node="remove" attribute:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
  <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
  <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
  <!-- Other content -->
</manifest>
Step 2: Customize the image selection plugin, e.g., SystemImagePickerPlugin
public class SystemImagePickerPlugin implements IPluginModule, IPluginRequestPermissionResultCallback {
    private static final String TAG = "SystemImagePickerPlugin";
    private ConversationIdentifier conversationIdentifier;

    @Override
    public Drawable obtainDrawable(Context context) {
        return context.getResources().getDrawable(R.drawable.rc_ext_plugin_image_selector);
    }

    @Override
    public String obtainTitle(Context context) {
        return context.getString(R.string.rc_ext_plugin_image);
    }

    @Override
    public void onClick(Fragment currentFragment, RongExtension extension, int index) {
        if (extension == null) {
            RLog.e(TAG, "onClick extension null");
            return;
        }
        conversationIdentifier = extension.getConversationIdentifier();

        FragmentActivity activity = currentFragment.getActivity();
        if (activity == null || activity.isDestroyed() || activity.isFinishing()) {
            RLog.e(TAG, "onClick activity null");
            return;
        }
        // This step is crucial; ensure it is implemented correctly
        int requestCode = ((index + 1) << 8) + (PictureConfig.CHOOSE_REQUEST & 0xff);
        // Use the system photo picker
        Intent intent = new Intent(Intent.ACTION_PICK);
        intent.setType("*/*"); // Supports both images and videos
        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
        currentFragment.startActivityForResult(intent, requestCode);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (data != null) {
            if (conversationIdentifier == null) {
                RLog.e(
                        TAG,
                        "onActivityResult conversationIdentifier is null, requestCode="
                                + requestCode
                                + ", resultCode="
                                + resultCode);
                return;
            }

            Uri selectedUri = data.getData();
            if (selectedUri != null) {
                String mimeType = IMCenter.getInstance().getContext().getContentResolver().getType(selectedUri);
                String path = FileUtils.getPathFromUri(IMCenter.getInstance().getContext(), selectedUri);

                LocalMedia localMedia = new LocalMedia();
                localMedia.setPath(path);
                localMedia.setMimeType(mimeType);

                if (mimeType != null && mimeType.startsWith("image")) {
                    // Send image
                    SendImageManager.getInstance().sendImage(conversationIdentifier, localMedia, false);
                    if (conversationIdentifier.getType().equals(Conversation.ConversationType.PRIVATE)) {
                        RongIMClient.getInstance()
                                .sendTypingStatus(
                                        conversationIdentifier.getType(),
                                        conversationIdentifier.getTargetId(),
                                        "RC:ImgMsg");
                    }
                } else if (mimeType != null && mimeType.startsWith("video")) {
                    // Send video
                    localMedia.setDuration(FileUtils.getVideoDuration(IMCenter.getInstance().getContext(), selectedUri));
                    SendMediaManager.getInstance()
                            .sendMedia(
                                    IMCenter.getInstance().getContext(),
                                    conversationIdentifier,
                                    selectedUri,
                                    localMedia.getDuration());
                    if (conversationIdentifier.getType().equals(Conversation.ConversationType.PRIVATE)) {
                        RongIMClient.getInstance()
                                .sendTypingStatus(
                                        conversationIdentifier.getType(),
                                        conversationIdentifier.getTargetId(),
                                        "RC:SightMsg");
                    }
                }
            }
        }
    }

    @Override
    public boolean onRequestPermissionResult(
            Fragment fragment,
            RongExtension extension,
            int requestCode,
            @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        // No additional permission checks needed for the system photo picker, keep this empty
        return true;
    }
}

public class FileUtils {

    /**
     * Get file path from Uri
     *
     * @param context Context
     * @param uri File Uri
     * @return File path
     */
    public static String getPathFromUri(Context context, Uri uri) {
        if (uri == null) {
            return null;
        }

        // If Uri is of file type, return the path directly
        if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        // If Uri is of content type, get the path from the content resolver
        if ("content".equalsIgnoreCase(uri.getScheme())) {
            Cursor cursor = null;
            try {
                String[] projection = {MediaStore.MediaColumns.DATA};
                cursor = context.getContentResolver().query(uri, projection, null, null, null);
                if (cursor != null && cursor.moveToFirst()) {
                    int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
                    return cursor.getString(columnIndex);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        }

        return null;
    }

    /**
     * Check if a file exists
     *
     * @param context Context
     * @param uri File Uri
     * @return Whether the file exists
     */
    public static boolean isFileExistsWithUri(Context context, Uri uri) {
        String path = getPathFromUri(context, uri);
        return !TextUtils.isEmpty(path) && new File(path).exists();
    }

    /**
     * Get video duration
     *
     * @param context Context
     * @param uri Video file Uri
     * @return Video duration in milliseconds
     */
    public static long getVideoDuration(Context context, Uri uri) {
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        try {
            retriever.setDataSource(context, uri);
            String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
            if (!TextUtils.isEmpty(durationStr)) {
                return Long.parseLong(durationStr);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            retriever.release();
        }
        return 0;
    }
}
Step 3: Customize CustomExtensionConfig and replace the default ImagePlugin
// 1. Customize CustomExtensionConfig
public class CustomExtensionConfig extends DefaultExtensionConfig {
    @Override
    public List<IPluginModule> getPluginModules(
            Conversation.ConversationType conversationType, String targetId) {
        List<IPluginModule> pluginList = super.getPluginModules(conversationType, targetId);
        
        Iterator<IPluginModule> iterator = pluginList.iterator();
        while (iterator.hasNext()) {
            IPluginModule pluginModule = iterator.next();
            if (pluginModule instanceof ImagePlugin) {
                iterator.remove();
            }
        }

        pluginList.add(0, new SystemImagePickerPlugin());
        return pluginList;
    }
}

// 2. Replace during initialization (no direct timing dependency)
RongExtensionManager.getInstance().setExtensionConfig(new CustomExtensionConfig());

More support

If you have any questions, feel free to submit a ticket.