Android targetSdkVersion从23升级到26适配指南

根据华为开发者平台AppGallery目标 API等级(targetSdkVersion)重要变更要求的通知,自2018年7月18日,华为应用市场联合国内主流应用预置与分发服务提供者,作为发起单位,共同签署电信终端产业协会(TAF)发布的《移动应用软件高API等级预置与分发自律公约》。按照公约规定,自2019年5月1日起,华为应用市场新上架应用应基于Android 8.0 (API等级26,即targetSdkVersion大于等于26)及以上开发。自2019年8月1日起,现有应用的更新应基于Android 8.0 (API等级26,即targetSdkVersion大于等于26)及以上开发。

这里对本次适配过程中遇到的适配内容做一次总结。

一、自动安装更新适配

     在Android 8.0以后,Google对第三方app安装apk进行了严格的限制,新增了android.permission.REQUEST_INSTALL_PACKAGES权限,如果你的应用想要获得安装apk的权限,必须在manifest中声明此权限。 由于Android向下兼容的关系,targetSdkVersion低于26则不用关心这个,但是现在我们需要适配到26+,如果我们有应用内更新或者下载安装其他apk的需求,就必须要关注安装权限了。

<!--适配Android8.0未知来源应用安装权限--> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

二、FileUriExposedException异常适配

    在targetSdkVersion升级到26之后,在读取媒体库文件(例如调用第三方应用打开PDF文件等)、安装apk的时候会出现崩溃,报错FileUriExposedException。Google认为通过诸如file://URI这样的URI访问文件是不安全的,特别是访问其它应用的私有目录和文件,因此很早就提供了FileProvider这样的东西用于管理文件访问。在Android 7.0+的系统上,Android SDK的 StrictMode 不再允许在应用外部公开file://URI,如果携带file://URI离开自己的应用(访问PackageInstaller,访问相册 etc.),就会抛出FileUriExposedException

 该问题有两种适配方案:

1.FileProvider适配

 在res目录下新建一个xml文件夹,并且新建一个provider_paths的xml文件

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path path="." name="camera_photos" />
    <external-path name="images" path="Pictures/" />
    <external-path name="dcim" path="DCIM/" />
</paths>

在AndroidManifest.xml中添加如下代码:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    <application
        ...
        <provider

             android:name="android.support.v4.content.FileProvider"

             android:authorities="${applicationId}.fileprovider"

             android:exported="false"

            android:grantUriPermissions="true">

            <meta-data

                 android:name="android.support.FILE_PROVIDER_PATHS"

                 android:resource="@xml/file_paths" />

        </provider>
    </application>
</manifest>

适配代码:

Uri uri;
if (Build.VERSION.SDK_INT >= 24) {
    uri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", new File(path));
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
    uri = Uri.fromFile(new File(path));
}

2.重新设置StrictMode,让VM忽略URI检查 (此方法不推荐)

适配代码:
if (Build.VERSION.SDK_INT >= 24) {
   StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
   StrictMode.setVmPolicy(builder.build());
}
Uri uri = Uri.fromFile(new File(path));

三、通知栏适配

从Android 8.0系统开始,Google引入了通知渠道这个概念。

什么是通知渠道呢?顾名思义,就是每条通知都要属于一个对应的渠道。每个App都可以自由地创建当前App拥有哪些通知渠道,但是这些通知渠道的控制权都是掌握在用户手上的。用户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动、或者是否要关闭这个渠道的通知。

对于每个App来说,通知渠道的划分是非常需要仔细考究的,因为通知渠道一旦创建之后就不能再修改了,因此开发者需要仔细分析自己的App一共有哪些类型的通知,然后再去创建相应的通知渠道。

当我们项目中的targetSdkVersion指定到了26或者更高,那么Android系统就会认为你的App已经做好了8.0系统的适配工作,当然包括了通知栏的适配。这个时候如果还不使用通知渠道的话,那么你的App的通知将完全无法弹出。

适配代码:

Notification.Builder mBuilder;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    NotificationChannel channel = new NotificationChannel("Notification","通知消息",NotificationManager.IMPORTANCE_HIGH);
    nm.createNotificationChannel(channel);
    mBuilder = new Notification.Builder(PushIntentService.this, "Notification");
} else {
    mBuilder = new Notification.Builder(PushIntentService.this);
}

需要注意的是,创建一个通知渠道至少需要渠道ID、渠道名称以及重要等级这三个参数,其中渠道ID可以随便定义,只要保证全局唯一性就可以。渠道名称是给用户看的,需要能够表达清楚这个渠道的用途。重要等级的不同则会决定通知的不同行为,当然这里只是初始状态下的重要等级,用户可以随时手动更改某个渠道的重要等级,App是无法干预的。其中重要等级共有IMPORTANCE_HIGH、IMPORTANCE_DEFAULT、IMPORTANCE_LOW、IMPORTANCE_MAX、IMPORTANCE_MIN、IMPORTANCE_NONE、IMPORTANCE_UNSPECIFIED,分别对应不同的通知重要程度。

四、悬浮窗适配

在Android 6.0已经将浮窗划为运行时权限,而且与普通的的运行时权限不大相同,属于特殊权限,浮窗权限需要使用Intent唤起应用设置页面让用户开启,然后在onActivityResult()中判断是否拥有权限。

// 判断是否拥有浮窗权限
boolean hasPermission = Settings.canDrawOverlays(context);
// 跳转权限申请界面

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M&&!hasPermission) {
    ToastTools.showShort(context, "请开启悬浮窗权限!");
    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
    getCurrentActivity().startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
}else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT&&!hasPermission){
    ToastTools.showShort(context, "请在\"权限管理\">\"悬浮窗\"开启权限!");
    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    Uri uri = Uri.fromParts("package", context.getPackageName(), null);
    intent.setData(uri);
    getCurrentActivity().startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
}

在Android 8.0中,对于浮窗进行了更严格的区分和限制,需要增加额外的悬浮窗type判断:

WindowManager.LayoutParams params = new WindowManager.LayoutParams();
if (Build.VERSION.SDK_INT>=26) {//8.0新特性
    params.type= WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}else{
    params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
}

 五、内容观察者ContentObserver短信监听注册适配

targetSdkVersion=23时,可直接调用如下代码注册内容观察者,后续可根据需求动态申请读取短信权限实现短信自动填充功能。

ContentResolver resolver = activity.getContentResolver();
ContentObserver observer = new SMSObserver(resolver, new Handler(activity));
resolver.registerContentObserver(URI, true, observer);

  在targetSdkVersion升级到26之后,在未动态申请短信权限时,直接注册如上的内容观察者,会导致应用Crash,需先动态申请短信权限,方可注册内容观察者,适配代码如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
        && activity.checkSelfPermission(Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED
        && activity.checkSelfPermission(Manifest.permission.RECEIVE_SMS) != PackageManager.PERMISSION_GRANTED) {
    activity.requestPermissions(new String[]{Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_SMS}, 0x123);
    setOnSMS(new OnSMS() {
        @Override
        public void onOk() {
            ContentResolver resolver = activity.getContentResolver();
            ContentObserver observer = new SMSObserver(resolver, new Handler(activity));
            resolver.registerContentObserver(URI, true, observer);
        }

        @Override
        public void onNo() {
            ToastTools.showShort(activity, "读取短信权限未开启!");
        }
    });
}

六、BroadcastReceiver无法接收广播适配

在targetSdkVersion升级到26之后需要setComponent才能收到广播消息,其中,ComponentName接收两个参数,参数1是自定义广播的包名,参数2是自定义广播的路径。

Intent intent = new Intent("xx.xx.xx.xx.pushinfoboast");
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
    intent.setComponent(new ComponentName(getApplicationContext().getPackageName(), "xx.xx.xx.xx.xx.SmsReceiver"));
}

七、抓包(代理网络请求)适配

对于面向Android7.0及以上系统的应用无法通过Fiddler、charles抓包的问题,默认情况下,来自所有应用的安全连接(使用TLS和HTTPS之类的协议)均信任预装的系统CA,而面向6.0及以下系统版本的应用默认情况下还会信任用户添加的CA证书。如果我们将targetSdkVersion修改到24及以上的时候,应用则不会信任用户安装的证书了。

这时,当我们通过Fiddler、charles或其他抓包工具抓取应用的Https请求时,尽管在手机上暗安装了Fiddler、charles证书,则仍会显示证书链不被信任。

对于该问题,针对不同的场景,有两种适配方案:

1.在res-xml文件夹下创建network-security-config.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates src="system" overridePins="true"/>
            <certificates src="user" overridePins="true"/>
        </trust-anchors>
    </base-config>
</network-security-config>

通过network_security_config.xml文件可以自定义网络安全设置,包括如下功能:

自定义信任的证书机构:适用于自签名证书或限制应用信任系统预装证书
证书固定:将应用的安全连接限制为特定的证书,适用于代理抓包、线下环境调试
明文通信选择退出:防止应用意外使用明文通信
仅调试重写:在debug状态设置上述限制

那么我们为了使我们的应用信任Fiddler、charles的证书,可以选择证书固定或者直接信任用户安装的证书。配置如下:

固定证书:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <trust-anchors>
            <certificates src="@raw/my_ca"/>
        </trust-anchors>
    </domain-config>
</network-security-config>

注: 以 PEM 或 DER 格式将自签署或非公共 CA 证书添加到 res/raw/my_ca。

信任用户安装的证书:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
   <trust-anchors>
       <certificates src="system"/>
       <certificates src="user"/>
   </trust-anchors>
</network-security-config>
但这样设置会忽视掉系统提供的安全的校验,所以尽可能不要配置到线上版本中,仅调试重写。

在manifest文件中配置application属性:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    <application

        ...

          android:networkSecurityConfig="@xml/network_security_config" >
        ...
    </application>
</manifest>

2.为网络请求库添加证书配置

由于我们不能动态控制debug状态,但是我们能控制什么时候为网络请求库配置https证书。例如:仅线上环境时不配置charles证书,其他时候由使用者指定加载charles证书文件。这种方式就做到了不依赖debuggable环境,而且可以动态修改证书文件。

不同的网络客户端配置证书方式,这里以okhttp为例做说明。

第一步:为你的网络客户端添加一个设置证书流的api,由调用者负责判断构建环境,并决定是否配置自定义证书。

    private List<InputStream> cerFileIsList;
    /**
     * 设置Https证书文件
     *
     * @param cerFileIs 证书文件流列表
     */
    public void addCertFileIs(InputStream cerFileIs) {
        if (cerFileIsList == null) {
            cerFileIsList = new ArrayList<>(2);
        }
        cerFileIsList.add(cerFileIs);
    }
第二步,在初始化OkHttpClient时使用证书构建SSLSocketFactory
private SSLSocketFactory createSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance("TLS");

        if (cerFileIsList != null && cerFileIsList.size() > 0) {
            try {
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                //创建一个包含我们信任证书的KeyStore
                String keyStoreType = KeyStore.getDefaultType();
                KeyStore keyStore = KeyStore.getInstance(keyStoreType);
                keyStore.load(null, null);

                int certIndex = 0;
                for (InputStream inputStream : cerFileIsList) {
                    String certAlias = String.valueOf(certIndex++);
                    keyStore.setCertificateEntry(certAlias, cf.generateCertificate(inputStream));
                }
                //根据秘钥库生成信任管理器
                String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
                TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
                tmf.init(keyStore);
                sslContext.init(null, tmf.getTrustManagers(), null);
            } catch (IOException | CertificateException | KeyStoreException e) {
                e.printStackTrace();
            } finally {
                CloseUtils.closeIO(cerFileIsList);
                cerFileIsList.clear();
            }
        } else {
            sslContext.init(null, null, null);
        }

        return new Tls12SocketFactory(sslContext.getSocketFactory());
    }

当测试同学需要抓包时,只需要下载对应的Charles证书并由App载入后,重新初始化OkHttpClient就可以了。

通过OkHttpClient设置自定义证书时需要注意一点,就是需要把正常服务端接口域名的证书一并设置进来,否则的话App只信任charles证书,却不信任后端服务的证书了。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏文超IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值