一、前言
如下图,应用市场纷纷要求 targetSdkVersion 大于 26,于是乎将公司项目升级一波并测试上线,记录一下升级过程中觉得重要的点。
二、升级
2.1 动态权限
如果你的应用之前的targetSdkVersion < 23,那么升级targetSdkVersion到26+首先要做的就是适配运行时权限。Android 6.0引入了运行时权限机制,这已经过去2年多了,适配相关的文章、库已经有很多,这里就不再赘述。
2.2 FileProvider (重要)
在读取媒体库文件、安装 apk 的时候会出现崩溃,报错 FileUriExposedException。Google 认为通过诸如 file://URI 这样的 URI 访问文件是不安全的,特别是访问其它应用的私有目录和文件,因此很早就提供了FileProvider这样的东西用于管理文件访问,只不过由于向下兼容,老方法一直能用,关注寥寥。但是在 Android 7.0+ 的系统上,Android SDK 的 StrictMode 不再允许在应用外部公开 file://URI,如果携带 file://URI 离开自己的应用(访问PackageInstaller,访问相册),就会抛出 FileUriExposedException。
对于这个问题有两种解决方案:
1、老老实实的编写FileProvider,如何设置FileProvider网上已经有很多文章,这里不再赘述
2、重新设置StrictMode,让VM忽略URI检查,此方法为暂缓之计,如果适配工作量极大可以先行使用,建议逐步适配过渡到FileProvider。
// 在Application onCreate()期间执行
// 关闭FileUriExposedException检查
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
关于 FileProvider 的使用其实有点复杂,大家要仔细查一下怎么使用。在我们项目中主要要修改的地方就是下载升级app,这里贴出代码:
private void installApk(Context context, File filePath) {
String mimeDefault = "application/vnd.android.package-archive";
try {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(android.content.Intent.ACTION_VIEW);
//兼容7.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//这里牵涉到7.0系统中URI读取的变更
Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", filePath);
intent.setDataAndType(contentUri, mimeDefault);
} else {
intent.setDataAndType(Uri.fromFile(filePath), mimeDefault);
}
if (context.getPackageManager().queryIntentActivities(intent, 0).size() > 0) {
//如果APK安装界面存在,携带请求码跳转。使用forResult是为了处理用户 取消 安装的事件。
//外面这层判断理论上来说可以不要,但是由于国内的定制,这个加上还是比较保险的
((Activity) context).startActivityForResult(intent, AppUpdateBusiness.REQUEST_INSTALL_APP);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
注意,安装前还需要开启“安装未知来源应用”权限,下面马上会讲。
2.3 允许安装未知来源应用
在 Android 8.0 以后,Google 对第三方 app 安装 apk 进行了严格的限制,新增了 REQUEST_INSTALL_PACKAGES 权限,如果你的应用想要获得安装 apk 的权限,必须在 manifest 中声明此权限,并且在需要调用的时候执行类似于浮窗权限申请一样的权限申请流程。 由于 Android 向下兼容的关系,targetSdkVersion 低于 26 则不用关心这个,但是现在我们需要适配到 26+,如果我们有应用内更新或者下载安装其他 apk 的需求,就必须要关注安装权限了。具体代码如下:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
//兼容8.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = mContext.getPackageManager().canRequestPackageInstalls();
if (!hasInstallPermission) {
//后面跟上包名,可以直接跳转到对应APP的未知来源权限设置界面。
//使用startActivityForResult 是为了在关闭设置界面之后,获取用户的操作结果,然后根据结果做其他处理
//跳转到设置-允许安装未知来源-页面
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
intent.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID));
((Activity) mContext).startActivityForResult(intent, REQUEST_UNKNOWN_APP_SOURCES);
return;
}
}
2.4 通知渠道
Android 8.0以后增加了推送渠道(Notification Channel)的概念,用于更精细的划分一个应用的不同推送,比如一个类似知乎这样的以阅读为主的综合类应用,就可以将推送分为推广、私信等channel;用户不想看推广推送,又怕错过私信,就可以将推广关闭,保留私信的 channel。
我们知道 Android Notification 有 3 个必填属性:icon、title、content,这三个如果缺了任何一个都将会导致 Notification 不显示,而对于 targetSdkVersion = 26+ 的应用,又增加了一个 NotificationChannelId,如果构建 Notification 的时候不设置NotificationChannelId,后果会比不设置前三个还严重,将会在显示 notification 时崩溃。所以适配26+我们必须对应用的NotificationChannel 进行管理。
Notification Channel 由开发者创建,构建时的配置与普通 Notification 相似,可以设定重要等级、是否震动、是否响铃等,
public static NotificationCompat.Builder buildNotification(String channelId, String channelTitle) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (TextUtils.isEmpty(channelId) || TextUtils.isEmpty(channelTitle)) {
channelId = DEFAULT_CHANNEL_ID;
channelTitle = DEFAULT_CHANNEL_NAME;
}
NotificationManager mNotificationManager = getNotificationManager();
NotificationChannel mChannel = mNotificationManager.getNotificationChannel(channelId);
if (mChannel == null) {
mChannel = new NotificationChannel(channelId, channelTitle, NotificationManager.IMPORTANCE_DEFAULT);
mChannel.setSound(null, null);
mNotificationManager.createNotificationChannel(mChannel);
}
return new NotificationCompat.Builder(mContext, mChannel.getId());
}
return new NotificationCompat.Builder(mContext);
}
需要注意的是NotificationChannel一旦创建,则不再能够被应用修改,只有用户可以修改。所以在调试 NotificationChannel 时,记得卸载重装。
2.5 分享图片到第三方app
Android 8.0 后,分享图片到第三方 app(比如微信、QQ、微博、朋友圈、QQ空间)都有可能会出现莫名的 bug,所以如果想一了百了,可以用下面这种终极分享的方法,代码如下:
/**
* 终极分享,适用于任何图片分享。
*/
public static void originalShareImage(Context context, List<File> files) {
Intent share_intent = new Intent();
ArrayList<Uri> imageUris = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
for (File f : files) {
Uri imageContentUri = getImageContentUri(context, f);
imageUris.add(imageContentUri);
}
} else {
for (File f : files) {
imageUris.add(Uri.fromFile(f));
}
}
share_intent.setAction(Intent.ACTION_SEND_MULTIPLE);//设置分享行为
share_intent.setType("image/*");//设置分享内容的类型
share_intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
share_intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
share_intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris);
context.startActivity(Intent.createChooser(share_intent, "Share"));
}
private static Uri getImageContentUri(Context context, File imageFile) {
String filePath = imageFile.getAbsolutePath();
Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID}, MediaStore.Images.Media.DATA + "=? ",
new String[]{filePath}, null);
if (cursor != null && cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
cursor.close();
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (cursor != null) {
cursor.close();
}
if (imageFile.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
2.6 https抓包
说到抓包,这就是涉及到调试和安全问题,对于 Android 7.0 (API 24 )以下,你可以直接使用 Charles 安装相关证书配置好代理后直接实现。但是在 Android 7.0 之后,google 推出更加严格的安全机制,用户添加的CA证书不能再用于安全连接。
至于具体怎么配置,Google 官方给出超级全面解释,官方连接。这里简单说下,需要在 manifest 中添加一个 network_security_config。
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:networkSecurityConfig="@xml/network_security_config"
... >
...
</application>
</manifest>
简单分析下,为什么在 7.0 之后,在手机内直接安装证书没效果呢?这是 6.0 默认配置:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
然后这是 7.0 默认配置:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
区别显而易见,我们在手机里自己安装证书,就是对应在 user 域,对于 7.0 来说,默认是直接不信任。所以你装代理证书失去意义。那要解决 7.0 不能抓包调试这个问题,你可以直接配置出这文件,选择上面 6.0 默认配置方式,信任来自 user 域证书。这样,你就又可以抓包调试。当然,这里如果想要更加安全的话,还有很多其他做法,具体的大家可以去看官方文档。
2.7 自适应启动图标
主要是让应用图标更好的适配各大平台手机,具体可以看这篇 一起来学习Android 8.0系统的应用图标适配吧。适配前后大概就是下面这个区别(借用网易考拉的图):
三、总结
以上是我在公司项目中遇到要修改的地方,当然有可能有一些小地方没有注意到,大家按照自己的项目进行必要的适配就可以了。
官方文档看起来很啰嗦费劲,这里推荐一些参考链接: