前言
一杯茶、一包烟、一个Bug改一天!某天,当我正在沉浸在搬砖的快乐中,突然收到了一张图片,是这样的
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();
}
总结
由于本项目涉及的问题大致就这么多,由于时间很仓促,所以整体就是哪出问题就改哪里,但也要心里又大致的预期,知道那些地方可能会出问题,并做出合理的排期。如果条件允许的话,还是要早做适配要好,避免这种突然的政策弄的措手不及!