背景:Google 新政策
Google 在 2023 年 10 月公布了新的“照片和视频权限”政策,并要求开发者在 2024 年 8 月 31 日之前对权限进行调整。
时间表信息
-
** 2023 年 10 月**: 公布新的“照片和视频权限”政策。
-
** 2024 年 8 月 31 日**:
-
如果应用只用一次或很少使用照片,必须从应用清单中移除
READ_MEDIA_IMAGES
和READ_MEDIA_VIDEO
权限,并在必要时改为使用系统照片选择器。 -
如果需要时间撤消权限或改用选择器,可以申请延期至 2025 年 1 月。
-
如果应用具有核心使用情形或广泛访问权限使用情形,开发者可以使用 Google Play 管理中心内的声明表单,提供相应使用情形需要这些权限的依据。
-
解决方案
场景一:应用中的 IM 属于高频使用场景
xxx App 属于高频使用多媒体功能的应用。如果你的应用具有核心使用情形或广泛访问权限使用情形,需要频繁使用多媒体功能,可以通过 Google Play 管理中心内的声明表单,提供相应使用情形需要这些权限的依据,从而保留这些权限。
申请保留权限
-
访问 Google Play 管理中心: 登录 Google Play 管理中心,找到相关应用。
-
填写声明表单: 提供应用需要高频使用多媒体权限的详细说明。
-
提交审核: 提交表单并等待 Google 的审核结果。
发布应用的时候 提示让用户增加权限说明的视频链接参考:权限说明视频链接 (网络需要 VPN )
场景二:应用中的 IM属于低频使用场景(或者谷歌审核不通过的)
步骤 1:移除多媒体权限
你可以在你的应用的 AndroidManifest.xml
文件中通过tools:node="remove"
属性来移除这些权限声明:
<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"/>
<!-- 其他内容 -->
</manifest>
步骤 2:自定义 图片选择的 IPluginModule 插件,比如: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;
}
// 这个很重要,不要写错
int requestCode = ((index + 1) << 8) + (PictureConfig.CHOOSE_REQUEST & 0xff);
// 使用系统照片选择器
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("*/*"); // 支持图片和视频
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")) {
// 发送图片
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")) {
// 发送视频
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) {
// 系统照片选择器不需要额外权限检查,保持空实现即可
return true;
}
}
public class FileUtils {
/**
* 从 Uri 获取文件路径
*
* @param context 上下文
* @param uri 文件的 Uri
* @return 文件的路径
*/
public static String getPathFromUri(Context context, Uri uri) {
if (uri == null) {
return null;
}
// 如果 Uri 是文件类型,直接返回路径
if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
// 如果是内容类型,从内容解析器中获取路径
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;
}
/**
* 检查文件是否存在
*
* @param context 上下文
* @param uri 文件的 Uri
* @return 是否存在
*/
public static boolean isFileExistsWithUri(Context context, Uri uri) {
String path = getPathFromUri(context, uri);
return !TextUtils.isEmpty(path) && new File(path).exists();
}
/**
* 获取视频的时长
*
* @param context 上下文
* @param uri 视频文件的 Uri
* @return 视频时长(毫秒)
*/
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;
}
}
步骤 3:自定义 CustomExtensionConfig ,并替换默认的 ImagePlugin
// 1. 自定义 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.初始化时替换(和初始化时序没有直接关系)
RongExtensionManager.getInstance().setExtensionConfig(new CustomExtensionConfig());
更多支持
如有疑问,欢迎提交工单。