根据华为开发者平台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的需求,就必须要关注安装权限了。
在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文件
在AndroidManifest.xml中添加如下代码:
android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
适配代码:
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; }
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, "读取短信权限未开启!"); } }); }
在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文件:
通过network_security_config.xml文件可以自定义网络安全设置,包括如下功能:
自定义信任的证书机构:适用于自签名证书或限制应用信任系统预装证书
证书固定:将应用的安全连接限制为特定的证书,适用于代理抓包、线下环境调试
明文通信选择退出:防止应用意外使用明文通信
仅调试重写:在debug状态设置上述限制
那么我们为了使我们的应用信任Fiddler、charles的证书,可以选择证书固定或者直接信任用户安装的证书。配置如下:
固定证书:
注: 以 PEM 或 DER 格式将自签署或非公共 CA 证书添加到 res/raw/my_ca。
信任用户安装的证书:
但这样设置会忽视掉系统提供的安全的校验,所以尽可能不要配置到线上版本中,仅调试重写。
在manifest文件中配置application属性:
... android:networkSecurityConfig="@xml/network_security_config" >
...
2.为网络请求库添加证书配置
由于我们不能动态控制debug状态,但是我们能控制什么时候为网络请求库配置https证书。例如:仅线上环境时不配置charles证书,其他时候由使用者指定加载charles证书文件。这种方式就做到了不依赖debuggable环境,而且可以动态修改证书文件。
不同的网络客户端配置证书方式,这里以okhttp为例做说明。
第一步:为你的网络客户端添加一个设置证书流的api,由调用者负责判断构建环境,并决定是否配置自定义证书。
private List
/**
* 设置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证书,却不信任后端服务的证书了。