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
andREAD_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.
- If your app uses photos or videos infrequently, remove
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
- Access Google Play Console: Log in and locate your app.
- Fill out the declaration form: Provide detailed justification for high-frequency multimedia usage.
- 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.