谷歌图片权限解决方案

背景: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);

                if (mimeType != null && mimeType.startsWith("image")) {
                    // 发送图片
                    CustomSendImageManager.getInstance().sendImage(conversationIdentifier, selectedUri);
                    if (conversationIdentifier.getType().equals(Conversation.ConversationType.PRIVATE)) {
                        RongIMClient.getInstance()
                                .sendTypingStatus(
                                        conversationIdentifier.getType(),
                                        conversationIdentifier.getTargetId(),
                                        "RC:ImgMsg");
                    }
                } else if (mimeType != null && mimeType.startsWith("video")) {
                    // 发送视频
                    long videoDuration = FileUtils.getVideoDuration(IMCenter.getInstance().getContext(), selectedUri);
                    SendMediaManager.getInstance()
                            .sendMedia(
                                    IMCenter.getInstance().getContext(),
                                    conversationIdentifier,
                                    selectedUri,
                                    videoDuration);
                    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 CustomSendImageManager {
    private static final String TAG = "SendImageManager";

    private ExecutorService executorService;
    private final UploadController uploadController;

    static class SingletonHolder {
        static CustomSendImageManager sInstance = new CustomSendImageManager();
    }

    public static CustomSendImageManager getInstance() {
        return SingletonHolder.sInstance;
    }

    private CustomSendImageManager() {
        executorService = getExecutorService();
        uploadController = new UploadController();
    }

    public void sendImage(
            ConversationIdentifier conversationIdentifier, Uri selectedUri) {
        String mimeType = IMCenter.getInstance().getContext().getContentResolver().getType(selectedUri);

        MessageContent content;
        if (PictureMimeType.isGif(mimeType)) {
            content = GIFMessage.obtain(selectedUri);
        } else {
            content = ImageMessage.obtain(selectedUri);
        }
        if (DestructManager.isActive()) {
            if (content != null) {
                content.setDestruct(true);
                content.setDestructTime(DestructManager.IMAGE_DESTRUCT_TIME);
            }
        }
        IMCenter.getInstance()
                .insertOutgoingMessage(
                        conversationIdentifier,
                        Message.SentStatus.SENDING,
                        content,
                        System.currentTimeMillis(),
                        new RongIMClient.ResultCallback<Message>() {
                            @Override
                            public void onSuccess(Message message) {
                                uploadController.execute(message);
                            }

                            @Override
                            public void onError(RongIMClient.ErrorCode errorCode) {
                                // do nothing
                            }
                        });
    }


    private class UploadController implements Runnable {
        final List<Message> pendingMessages;
        Message executingMessage;

        public UploadController() {
            this.pendingMessages = new ArrayList<>();
        }

        public void execute(Message message) {
            synchronized (pendingMessages) {
                pendingMessages.add(message);
                if (executingMessage == null) {
                    executingMessage = pendingMessages.remove(0);
                    executorService.submit(this);
                }
            }
        }

        private void polling() {
            synchronized (pendingMessages) {
                RLog.d(TAG, "polling " + pendingMessages.size());
                if (!pendingMessages.isEmpty()) {
                    executingMessage = pendingMessages.remove(0);
                    executorService.submit(this);
                } else {
                    executingMessage = null;
                }
            }
        }

        @Override
        public void run() {
            boolean isDestruct = false;
            if (executingMessage.getContent() != null)
                isDestruct = executingMessage.getContent().isDestruct();
            IMCenter.getInstance()
                    .sendMediaMessage(
                            executingMessage,
                            isDestruct
                                    ? IMCenter.getInstance()
                                            .getContext()
                                            .getString(
                                                    R.string.rc_conversation_summary_content_burn)
                                    : null,
                            null,
                            new IRongCallback.ISendMediaMessageCallback() {
                                @Override
                                public void onAttached(Message message) {
                                    // default implementation ignored
                                }

                                @Override
                                public void onError(Message message, RongIMClient.ErrorCode code) {
                                    polling();
                                }

                                @Override
                                public void onSuccess(Message message) {
                                    polling();
                                }

                                @Override
                                public void onProgress(Message message, int progress) {
                                    // do nothing
                                }

                                @Override
                                public void onCanceled(Message message) {
                                    // do nothing
                                }
                            });
        }
    }

    private ExecutorService getExecutorService() {
        if (executorService == null) {
            executorService =
                    new ThreadPoolExecutor(
                            1,
                            Integer.MAX_VALUE,
                            60,
                            TimeUnit.SECONDS,
                            new SynchronousQueue<Runnable>(),
                            threadFactory());
        }
        return executorService;
    }

    private ThreadFactory threadFactory() {
        return new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread result = new Thread(runnable, "Rong SendMediaManager");
                result.setDaemon(false);
                return result;
            }
        };
    }
}

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());

更多支持

如有疑问,欢迎提交工单