文档章节

开源中国客户端 Android 10 经验适配指南,含代码

黄海彬
 黄海彬
发布于 09/16 11:05
字数 1643
阅读 8048
收藏 108

我们App的适配从 targetSdkVersion = 26跨版本升级到29,因此会遇到大量的坑,最终的版本配置如下:

现在进入填坑适配指南,包含实际经验代码,绝不照搬翻译文档

1.Region.Op相关异常:java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed

当 targetSdkVersion >= Build.VERSION_CODES.P 时调用 canvas.clipPath(path, Region.Op.XXX); 引起的异常,参考源码如下:

@Deprecated
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op) {
     checkValidClipOp(op);
     return nClipPath(mNativeCanvasWrapper, path.readOnlyNI(), op.nativeInt);
}

private static void checkValidClipOp(@NonNull Region.Op op) {
     if (sCompatiblityVersion >= Build.VERSION_CODES.P
         && op != Region.Op.INTERSECT && op != Region.Op.DIFFERENCE) {
         throw new IllegalArgumentException(
                    "Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed");
     }
}

我们可以看到当目标版本从Android P开始,Canvas.clipPath(@NonNull Path path, @NonNull Region.Op op) ; 已经被废弃,而且是包含异常风险的废弃API,只有 Region.Op.INTERSECT 和 Region.Op.DIFFERENCE 得到兼容,目前不清楚google此举目的如何,仅仅如此简单就抛出异常提示开发者适配,几乎所有的博客解决方案都是如下简单粗暴:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    canvas.clipPath(path);
} else {
    canvas.clipPath(path, Region.Op.XOR);// REPLACE、UNION 等
}

但我们一定需要一些高级逻辑运算效果怎么办?如小说的仿真翻页阅读效果,解决方案如下,用Path.op代替,先运算Path,再给canvas.clipPath:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
    Path mPathXOR = new Path();
    mPathXOR.moveTo(0,0);
    mPathXOR.lineTo(getWidth(),0);
    mPathXOR.lineTo(getWidth(),getHeight());
    mPathXOR.lineTo(0,getHeight());
    mPathXOR.close();
    //以上根据实际的Canvas或View的大小,画出相同大小的Path即可
    mPathXOR.op(mPath0, Path.Op.XOR);
    canvas.clipPath(mPathXOR);
}else {
    canvas.clipPath(mPath0, Region.Op.XOR);
}

2.明文HTTP限制

当 targetSdkVersion >= Build.VERSION_CODES.P 时,默认限制了HTTP请求,并出现相关日志:

java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy

第一种解决方案:在AndroidManifest.xml中Application添加如下节点代码

<application android:usesCleartextTraffic="true">

第二种解决方案:在res目录新建xml目录,已建的跳过 在xml目录新建一个xml文件network_security_config.xml,然后在AndroidManifest.xml中Application添加如下节点代码

android:networkSecurityConfig="@xml/network_config"

名字随机,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

3.Android Q(10)中的媒体资源读写

相关的Android Q 行为变更不做细说,网上大部分博客关于Android Q 适配都在说行为变更,我们将根据实际遇到的问题,实际解决

1、扫描系统相册、视频等,图片、视频选择器都是通过ContentResolver来提供,主要代码如下:

private static final String[] IMAGE_PROJECTION = {
            MediaStore.Images.Media.DATA,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.BUCKET_ID,
            MediaStore.Images.Media.BUCKET_DISPLAY_NAME};

 Cursor imageCursor = mContext.getContentResolver().query(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    IMAGE_PROJECTION, null, null, IMAGE_PROJECTION[4] + " DESC");

String path = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[0]));
String name = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[1]));
int id = imageCursor.getInt(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[2]));
String folderPath = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[3]));
String folderName = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[4]));

//Android Q 公有目录只能通过Content Uri + id的方式访问,以前的File路径全部无效,如果是Video,记得换成MediaStore.Videos
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
      path  = MediaStore.Images.Media
                       .EXTERNAL_CONTENT_URI
                       .buildUpon()
                       .appendPath(String.valueOf(id)).build().toString();
 }

2、判断公有目录文件是否存在,自Android Q开始,公有目录File API都失效,不能直接通过new File(path).exists();判断公有目录文件是否存在,正确方式如下:

public static boolean isAndroidQFileExists(Context context, String path){
        if (context == null) {
            return false;
        }
        AssetFileDescriptor afd = null;
        ContentResolver cr = context.getContentResolver();
        try {
            Uri uri = Uri.parse(path);
            afd = cr.openAssetFileDescriptor(Uri.parse(path), "r");
            if (afd == null) {
                return false;
            } else {
                close(afd);
            }
        } catch (FileNotFoundException e) {
            return false;
        }finally {
            close(afd);
        }
        return true;
}

3、保存或者下载文件到公有目录,保存Bitmap同理,如Download,MIME_TYPE类型可以自行参考对应的文件类型,这里只对APK作出说明

public static void copyToDownloadAndroidQ(Context context, String sourcePath, String fileName, String saveDirName){
        ContentValues values = new ContentValues();
        values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
        values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
        values.put(MediaStore.Downloads.RELATIVE_PATH, "Download/" + saveDirName.replaceAll("/","") + "/");

        Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
        ContentResolver resolver = context.getContentResolver();

        Uri insertUri = resolver.insert(external, values);
        if(insertUri == null) {
            return;
        }

        String mFilePath = insertUri.toString();

        InputStream is = null;
        OutputStream os = null;
        try {
            os = resolver.openOutputStream(insertUri);
            if(os == null){
                return;
            }
            int read;
            File sourceFile = new File(sourcePath);
            if (sourceFile.exists()) { // 文件存在时
                is = new FileInputStream(sourceFile); // 读入原文件
                byte[] buffer = new byte[1444];
                while ((read = is.read(buffer)) != -1) {
                    os.write(buffer, 0, read);
                }
                is.close();
                os.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            close(is,os);
        }

}

4、保存图片相关

 /**
     * 通过MediaStore保存,兼容AndroidQ,保存成功自动添加到相册数据库,无需再发送广告告诉系统插入相册
     *
     * @param context      context
     * @param sourceFile   源文件
     * @param saveFileName 保存的文件名
     * @param saveDirName  picture子目录
     * @return 成功或者失败
     */
    public static boolean saveImageWithAndroidQ(Context context,
                                                  File sourceFile,
                                                  String saveFileName,
                                                  String saveDirName) {
        String extension = BitmapUtil.getExtension(sourceFile.getAbsolutePath());

        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
        values.put(MediaStore.Images.Media.DISPLAY_NAME, saveFileName);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
        values.put(MediaStore.Images.Media.TITLE, "Image.png");
        values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + saveDirName);

        Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        ContentResolver resolver = context.getContentResolver();

        Uri insertUri = resolver.insert(external, values);
        BufferedInputStream inputStream = null;
        OutputStream os = null;
        boolean result = false;
        try {
            inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
            if (insertUri != null) {
                os = resolver.openOutputStream(insertUri);
            }
            if (os != null) {
                byte[] buffer = new byte[1024 * 4];
                int len;
                while ((len = inputStream.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                }
                os.flush();
            }
            result = true;
        } catch (IOException e) {
            result = false;
        } finally {
            Util.close(os, inputStream);
        }
        return result;
}

4.EditText默认不获取焦点,不自动弹出键盘

该问题出现在 targetSdkVersion >= Build.VERSION_CODES.P 情况下,且设备版本为Android P以上版本,目前我们没有从源码中查到相关判断改动,解决方法在onCreate中加入如下代码:

mEditText.post(() -> {
       mEditText.requestFocus();
       mEditText.setFocusable(true);
       mEditText.setFocusableInTouchMode(true);
});

5.Only fullscreen activities can request orientation 异常

该问题出现在 targetSdkVersion >= Build.VERSION_CODES.O_MR1 ,也就是 API 27,当设备为Android 26时(27以上已经修复,也许google觉得不妥当,又改回来了),如果非全面屏透明activity固定了方向,则出现该异常,但是当我们在小米、魅族等Android 26机型测试的时候,并没有该异常,华为机型则报该异常,这是何等的卧槽。。。没办法,去掉透明style或者去掉固定方向代码即可,其它无解

6.安装APK Intent及其它文件相关Intent

/*
* 自Android N开始,是通过FileProvider共享相关文件,但是Android Q对公有目录 File API进行了限制
* 从代码上看,又变得和以前低版本一样了,只是必须加上权限代码Intent.FLAG_GRANT_READ_URI_PERMISSION
*/ 
private void installApk() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
            //适配Android Q,注意mFilePath是通过ContentResolver得到的,上述有相关代码
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(Uri.parse(mFilePath) ,"application/vnd.android.package-archive");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            startActivity(intent);
            return ;
        }

        File file = new File(saveFileName + "osc.apk");
        if (!file.exists())
            return;
        Intent intent = new Intent(Intent.ACTION_VIEW);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
		    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "net.oschina.app.provider", file);
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        startActivity(intent);
}

我们APK开发实践中暂时遇到的坑就这些,当然Android Q的改动是相当大的,例如还有App私有沙箱文件、定位权限和后台弹出Activity限制,这些都必须根据自身实践去踩坑适配,有条件的尽可能去阅读官方文档,参考改进。

© 著作权归作者所有

黄海彬

黄海彬

粉丝 88
博文 1
码字总数 1643
作品 0
深圳
程序员
私信 提问
加载中

评论(9)

南湖大道茶山刘
南湖大道茶山刘
好文,收藏了,支持支持😉
针叶
针叶
支持支持。
suzhou大魔王
suzhou大魔王
感谢大神分享
p
pysjping
现在客户端不开源了?
黄海彬
黄海彬 博主
稍后整理一下开源,有一段时间几个版本没开源了
睡不醒了睡
睡不醒了睡
请问:文中前面提到 判断公有目录是否存在的File API 都失效了,但是之后下载文件到公有目录时怎么还是使用了File.exist()来判断呢
黄海彬
黄海彬 博主
判断的那部分是私有目录或者私有沙盒文件,源文件来的,那个例子是copy的demo,将私有文件copy到公有目录,如果要下载,忽略源文件file即可
一个昵称而已
一个昵称而已
收藏收藏
宇润
宇润
膜拜安卓大神
开源中国 Android 客户端 v2.8.6 发布

这个美好的六月,开源中国源创会童鞋们马不停蹄地赶往杭州,因为「毕竟西湖六月中,风光不与四时同」,带着自由、开放、分享的开源精神和大家一起分享、交流,是何等惬意的事情呢!杭州附近的...

巴拉迪维
2017/06/05
2.9K
23
诚聘Ios/Android开发(上海知名券商)

Ios开发: Job Description 1、响应产品需求、完成IOS客户端相关内容的开发与设计; 2、新技术预研,协助完成方案选型和设计; 3、参与相关软件质量管理活动,确保设计、实现、测试工作按时保...

Jimmy007
2016/04/07
365
0
[北京]大型互联网公司招聘Android/iPhone客户端高级开发工程师 【猎头】

Android/iPhone客户端高级开发工程师 淘宝网-北京研发中心-无线开发 北京 工作年限 3年以上 学历要求 本科 岗位描述 根据需求进行Android/iPhone客户端软件的设计、开发和维护 规范编写设计和...

melody123
2012/05/16
126
0
oschina Android 客户端是怎么做适配的???

开源中国的 Android 客户端是怎么做适配的???我看了源代码也没找到不同分辨率的布局啊。。。?求大神告知下

ym_
2015/07/13
322
6
滴滴宣布开源 Android 端插件化框架 VirtualAPK

滴滴于今天正式宣布开源其 Android 插件化框架 —— VirtualAPK ,这也是滴滴公司的首个对外开源项目。 滴滴表示于去年开始研究 Android 插件化方面的技术,经过半年的开发、测试、适配和线上...

王练
2017/06/30
5.7K
16

没有更多内容

加载失败,请刷新页面

加载更多

移动端的弹窗滚动禁止body滚动

本文转载于:专业的前端网站➼移动端的弹窗滚动禁止body滚动 前言 最近一个需求是弹窗展示列表,显然是需要一个滚动条的,而滚动到底部就会穿透到body滚动,而阻止默认行为是不行的,这样两个...

前端老手
6分钟前
1
0
设计模式 建造者模式和模板方法模式扩展篇

建造者模式和模板方法模式扩展篇 UML 与抽象工厂模式比较 本模式可以看出与抽象工厂非常类似,都是产生不同的产品,怎么区分这两种设计的使用场景呢 - 建造者模式关注的是基本方法的调...

木本本
11分钟前
2
0
CPU 读取cache、内存、磁盘性能

google 工程师Jeff Dean 首先在他关于分布式系统的ppt文档列出来的,到处被引用的很多。 1纳秒等于10亿分之一秒,= 10 ^ -9 秒 ----------------------------------------------------------...

SibylY
19分钟前
2
0
在windows virtualbox上安装LEDE

按照 官方指南 安装时遇到若干问题,做一下总结。 一、官方指南链接中的镜像文件安装后报错 snapshots/targets/x86/64 中的 lede-x86-64-combined-squashfs.img 文件安装后,进行网络配置报错...

元谷
27分钟前
2
0
《老子》_安身尘世间,做个明白人

1、无为而无不为 出自《道德经·第四十八章》。【译文】处于无为的境界之中,没有什么不能做到的。 2、知者不言,言者不知。 出自《道德经·第五十六章》。【译文】言语无法表达“道”的真意...

庭前云落
30分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部