谷歌图片权限解决方案

背景:Google 新政策

Google 在 2023 年 10 月公布了新的“照片和视频权限”政策,并要求开发者在 2024 年 8 月 31 日之前对权限进行调整。

时间表信息

  • ** 2023 年 10 月**: 公布新的“照片和视频权限”政策。

  • ** 2024 年 8 月 31 日**:

    • 如果应用只用一次或很少使用照片,必须从应用清单中移除 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 权限,并在必要时改为使用系统照片选择器。

    • 如果需要时间撤消权限或改用选择器,可以申请延期至 2025 年 1 月。

    • 如果应用具有核心使用情形或广泛访问权限使用情形,开发者可以使用 Google Play 管理中心内的声明表单,提供相应使用情形需要这些权限的依据

解决方案

场景一:应用中的 IM 属于高频使用场景

xxx App 属于高频使用多媒体功能的应用。如果你的应用具有核心使用情形或广泛访问权限使用情形,需要频繁使用多媒体功能,可以通过 Google Play 管理中心内的声明表单,提供相应使用情形需要这些权限的依据,从而保留这些权限。

申请保留权限

  1. 访问 Google Play 管理中心: 登录 Google Play 管理中心,找到相关应用。

  2. 填写声明表单: 提供应用需要高频使用多媒体权限的详细说明。

  3. 提交审核: 提交表单并等待 Google 的审核结果。

有关 Google Play 照片和视频权限政策的详细信息

发布应用的时候 提示让用户增加权限说明的视频链接参考:权限说明视频链接 (网络需要 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());

更多支持

如有疑问,欢迎提交工单