Android 版本Q、R适配

1,372 阅读5分钟

前言

一杯茶、一包烟、一个Bug改一天!某天,当我正在沉浸在搬砖的快乐中,突然收到了一张图片,是这样的

WeChat73abb2fcc00658b333e2ce37c3b0a5b6.png WTF! 干!

适配

AndroidQ的适配想当年刚发布消息时,也是引起了不小的轰动,其中最重要的就是分区存储,虽然也有很多其他方面的改动,当然那都是属于锦上添花,暂时不适配不会有什么影响,但分区存储就不一样了!但好在谷歌留了个后手,AndroidQ可以暂时使用requestLegacyExternalStorage属性关闭分区存储,为什么说是暂时呢?因为后续这个属性就失效了,对于一些已经经过N多个人手的项目,能小心翼翼的跑起来就不错了!所以像我这又懒又菜的人可能直接加上就完事了,适配个毛!哈哈!!! 分区存储的概念相信大家都耳熟能详了,在这里就不多逼逼了!因为本人参与的项目主要是面向海外市场,所以还是要看谷歌市场的脸色!所以这次无论如何都躲不过了!

Android R

当接到谷歌的邮件时,就匆匆浏览了下谷歌官方文档,发现Android R和Android Q在分区存储方面总体上差不多,但还是又区别的!比如Android R 除了使用 MediaStore API外,还可以使用 File API。 方案:Android 10用requestLegacyExternalStorage,Android 11用新API.采用这种方法可谓省时省力我只需要针对R版本进行适配。当然每个项目实际情况不同,需要适配的地方也有差异,笔者项目需要适配的地方主要分为:

    1.targetSdkVersion升到 30
    2.查询第三方应用包名(做系统分享会用到):
     <queries>
        <package android:name="xx包名xxx"/>
        <package android:name="xx包名xxx"/>
    </queries>
    3.项目中有自己的相册功能,主要是通过ContentResolver检索手机中的图片和视频,适配之前主要是通过
    contentResolver.query(QUERY_URI, projection, selection, selectionArgs, orderBy) 方法,但在Android R中,需要通过bundle参数去查询,还有orderBy参数中limit参数也是有区别的:
 public static Bundle createQueryArgsBundle(String selection, String[] selectionArgs, int limitCount, int offset) {
        Bundle queryArgs = new Bundle();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
            queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
            queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, MediaStore.Files.FileColumns._ID + " DESC");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                queryArgs.putString(ContentResolver.QUERY_ARG_SQL_LIMIT, limitCount + " offset " + offset);
            }
        }
        return queryArgs;
    }
    
    4.系统图片裁剪,由原先path改为uri
    5.位置权限变更等
    其他变更由于本项目暂未涉及,就不多赘述了。

其实做完以上工作,Android R适配基本就没什么问题了,在Android Q版本的手机上由于 requestLegacyExternalStorage属性,运行也没问题!

对于从Android Q升级到Android R 的用户版本,可以在清单文件中指定 android:preserveLegacyExternalStorage="true",如果之前存储的文件夹为外部存储时,判断版本时可以增加 Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
    && !Environment.isExternalStorageLegacy() 他表示版本大于等于Android 11,并且开启了分区存储

Android Q

如果如上所言,就不会有Android Q的适配了,所以问题就出在谷歌发的那封声明,谷歌应用市场每次都会检测到requestLegacyExternalStorage,不确定会不会由此带来什么特殊的影响,比如下架,设置每次都会发邮件警告,这对于产品来说,可能是无法接受的!所以为了解决这些问题,只能硬着头皮适配了! Android Q适配主要还是集中在分区存储了,像暗黑模式这些都不是必须适配的,由于时间关系,暂且搁置。 对于本项目来说,Android Q的适配还是集中在手机图片的检索方面,比如ContentResolver的MediaStore.Files.FileColumns.DATA方法就不能用了,只能用MediaStore.Files.FileColumns._ID,通过id再去获取路径;否则你检索到图片和视频就没有封面了。

/**
     * Android Q 
     *
     * @param id
     * @return path
     */
    private static String getPathByAndroid_Q(long id) {
        return QUERY_URI.buildUpon().appendPath(ValueOf.toString(id)).build().toString();
    }

在Android Q中,由于获取到的图片基本都是content://开头的文件形式, 而不是之前的android/data的app私有目录,所以在图片裁剪时就出了问题,比如在获取bitmap对象时,之前的方法是

BitmapFactory.decodeFile(selectImgBean.getImgUrl(), options);

其中的imgUrl就是传统的文件路径,比如android/data等形式,但在Android Q中imgUrl是content://或file:// 形式的uri,所以可以采用下面的形式:

try {
                InputStream inputStream = App.sApp.getContentResolver().openInputStream(Uri.parse(selectImgBean.getImgUrl()));
                BitmapFactory.decodeStream(inputStream, null, options);
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

还有上传头像时,上传oss时的路径,也需要适配

/**
     * 将URI转为图片的路径
     *
     * @param context
     * @param uri
     * @return
     */
    public static String getRealFilePath(final Context context, final Uri uri) {
        if (null == uri) {
            return null;
        }
        final String scheme = uri.getScheme();
        String data = null;
        if (scheme == null)
            data = uri.getPath();
        else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
            data = uri.getPath();
        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
            Cursor cursor = context.getContentResolver().query(uri,
                    new String[]{MediaStore.Images.ImageColumns.DATA}, null, null, null);
            if (null != cursor) {
                if (cursor.moveToFirst()) {
                    int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
                    if (index > -1) {
                        data = cursor.getString(index);
                    }
                }
                cursor.close();
            }
        }
        return data;
    }

将uri转为file的path,否则上传oss会报错。 其他修改的地方就是下载图片或文件时,修改保存的目录由公有目录改为App的私有目录,操作公有目录的图片或视频通过MediaStore形式。如果是图片,可能还会有保存图片到相册的需求,也需要作出修改,举个例子:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            String fileName = file.getName();
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
            values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
            ContentResolver contentResolver = activity.getContentResolver();
            Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            if (uri == null) {
                ToastManager.showShort(activity, MultiLang.getString("downloadFailed", R.string.downloadFailed));
                return;
            }

            try {
                OutputStream out = contentResolver.openOutputStream(uri);
                FileInputStream fis = new FileInputStream(file);
                FileUtils.copy(fis, out);
                fis.close();
                out.close();
                ToastManager.showShort(activity, MultiLang.getString("downloadSuccess", R.string.downloadSuccess));
            } catch (IOException e) {
                e.printStackTrace();
            }

总结

由于本项目涉及的问题大致就这么多,由于时间很仓促,所以整体就是哪出问题就改哪里,但也要心里又大致的预期,知道那些地方可能会出问题,并做出合理的排期。如果条件允许的话,还是要早做适配要好,避免这种突然的政策弄的措手不及!