前言
新年第一篇总结与分享~
离19年新年就剩一个月左右了,整个公司的项目也不像以往一样紧张、忙碌,周末同事和朋友聊得最多的就是抢票和年终奖了(然而我们公司并没有😭),为自己心疼一分钟。。。
总结一下18年吧,主要负责了4个商业项目,还参与了公司 Android 新框架的封装,整体来说算是稳步提高吧。
先说一下写这篇文章的背景吧,主要就是不太忙了,再加上客户要求适配 Android 9.0 (第一个客户主动提出来的),在看完9.0的适配之后,也对之前的进行了一些整理,废话不多说,请看干货!
Android6.0适配
权限适配
动态权限适配是 Android 6.0 最先开始的,也是 Android 系统对开发者影响最大的改动之一。
权限适配三连问
- Q: 是否 Android 6.0 所有权限都需要动态申请?
- A: 不是。只有属于危险权限的才需要申请。
- Q: 危险权限有哪些?
- A: 见下面“危险权限分组说明”
- Q: 危险权限是否需要一个一个申请?
- A: 在 Android 6.0 ~ Android 8.0,不需要。如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用,即对于同一组内的权限,只要有一个被同意,其他的都会被同意。在 Android 8.0 之后,此行为已被纠正。系统只会授予应用明确请求的权限。然而一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准,但是若没有请求相应的权限而进行操作的话就会出现应用 crash 的情况。
危险权限分组说明
权限组 | 权限名称 |
---|---|
CALENDAR | android.permission.READ_CALENDAR |
android.permission.WRITE_CALENDAR | |
CAMERA | android.permission.CAMERA |
CONTACTS | android.permission.READ_CONTACTS |
android.permission.WRITE_CONTACTS | |
android.permission.GET_ACCOUNTS | |
LOCATION | android.permission.ACCESS_FINE_LOCATION |
android.permission.ACCESS_COARSE_LOCATION | |
MICROPHONE | android.permission.RECORD_AUDIO |
PHONE | android.permission.READ_PHONE_STATE |
android.permission.CALL_PHONE | |
android.permission.READ_CALL_LOG | |
android.permission.ADD_VOICEMAIL | |
android.permission.WRITE_CALL_LOG | |
android.permission.USE_SIP | |
android.permission.PROCESS_OUTGOING_CALLS | |
SENSORS | android.permission.BODY_SENSORS |
SMS | android.permission.SEND_SMS |
android.permission.RECEIVE_SMS | |
android.permission.READ_SMS | |
android.permission.RECEIVE_WAP_PUSH | |
android.permission.RECEIVE_MMS | |
STORAGE | android.permission.READ_EXTERNAL_STORAGE |
android.permission.WRITE_EXTERNAL_STORAGE |
对应在清单文件中的展示如下:
<!--CALENDAR-->
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<!--CAMERA-->
<uses-permission android:name="android.permission.CAMERA"/>
<!--CONTACTS-->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<!--LOCATION-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!--MICROPHONE-->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--PHONE-->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.ADD_VOICEMAIL"/>
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<uses-permission android:name="android.permission.USE_SIP"/>
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
<!--SENSORS-->
<uses-permission android:name="android.permission.BODY_SENSORS"/>
<!--SMS-->
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
<!--STORAGE-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Android 7.0适配
应用间共享文件
在 targetSdkVersion 大于等于的 24 的 app 中,但是我们没有去适配 Android 7.0。那么在调用安装页面,或修改用户头像操作时,就会失败。那么就需要你去适配 Android 7.0或是将 targetSdkVersion 改为 24 以下(不推荐)。适配的方法这里就不细讲,大家可以看鸿洋大神的 Android 7.0 行为变更 通过FileProvider在应用间共享文件这篇文章。
APK signature scheme v2
Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。在默认情况下,Android Studio 2.2 和 Android Plugin for Gradle 2.2 会使用 APK Signature Scheme v2 和传统签名方案来签署您的应用。
说明:
- 只勾选V1签名就是传统方案签署,但是在 Android 7.0 上不会使用V2安全的验证方式。
- 只勾选V2签名7.0以下会显示未安装,Android 7.0 上则会使用了V2安全的验证方式。
- 同时勾选V1和V2则所有版本都没问题。
org.apache不支持问题
// build.gradle里面加上这句话
defaultConfig {
useLibrary 'org.apache.http.legacy'
}
SharedPreferences闪退
// MODE_WORLD_READABLE:Android 7.0以后不能使用这个获取,会闪退
// 应修改成MODE_PRIVATE
SharedPreferences read = getSharedPreferences(RELEASE_POOL_DATA, MODE_WORLD_READABLE);
Android 8.0适配
Android 8.0中PHONE权限组新增两个权限
ANSWER_PHONE_CALLS:允许您的应用通过编程方式接听呼入电话。要在您的应用中处理呼入电话,您可以使用 acceptRingingCall() 函数。
READ_PHONE_NUMBERS:权限允许您的应用读取设备中存储的电话号码。
通知适配
Android 8.0中,为了更好的管制通知的提醒,不想一些不重要的通知打扰用户,新增了通知渠道,用户可以根据渠道来屏蔽一些不想要的通知。
代码示例如下:
/**
* Description: Android 8.0通知的兼容类
* Author: Jack Zhang
* create on: 2019/1/2 3:16 PM
*/
public class MyNotification
{
public static final String CHANNEL_ID_GL = "com.jz.gailun";
public static final String CHANNEL_NAME_GL = "盖伦";
public static final String CHANNEL_ID_AX = "com.jz.aixi";
public static final String CHANNEL_NAME_AX = "艾希";
public static final String CHANNEL_ID_LL = "com.jz.liulang";
public static final String CHANNEL_NAME_LL = "流浪";
public static void setONotifyChannel(NotificationManager manager, String channeId, String channelName)
{
setONotifyChannel(manager, null, channeId, channelName);
}
public static void setONotifyChannel(NotificationManager manager, NotificationCompat.Builder builder, String channeId, String channelName)
{
if (TextUtils.isEmpty(channeId) || TextUtils.isEmpty(channelName))
Logger.e("Android 8.0 Notification的channeId与channelName不能为空");
if (Build.VERSION.SDK_INT >= 26)
{
// 第三个参数设置通知的优先级别
NotificationChannel channel = new NotificationChannel(channeId, channelName, NotificationManager.IMPORTANCE_DEFAULT);
// 是否可以绕过请勿打扰模式
channel.canBypassDnd();
// 是否可以显示icon角标
channel.canShowBadge();
// 是否显示通知闪灯
channel.enableLights(true);
// 收到消息时震动提示
channel.enableVibration(true);
// 设置绕过免打扰
channel.setBypassDnd(true);
channel.setLockscreenVisibility(NotificationCompat.VISIBILITY_SECRET);
// 设置闪光灯颜色
channel.setLightColor(Color.RED);
// 获取设置铃声设置
channel.getAudioAttributes();
// 设置震动模式
channel.setVibrationPattern(new long[]{100, 200, 100});
// 是否会闪光
channel.shouldShowLights();
if (manager != null)
manager.createNotificationChannel(channel);
if (builder != null)
builder.setChannelId(channeId);//这个id参数要与上面channel构建的第一个参数对应
}
}
public static Notification getNotification(Context context, String channelId)
{
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, channelId);
Notification notification = notificationBuilder.setOngoing(true)
.setSmallIcon(R.mipmap.ic_logo)
.setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.build();
return notification;
}
}
/**
* Description: 通知管理类
* Author: Jack Zhang
* create on: 2019/1/2 3:23 PM
*/
public class NotifyManager
{
private volatile static NotifyManager INSTANCE;
private NotifyManager(Context context)
{
initNotifyManager(context);
}
public static NotifyManager getInstance(Context context)
{
if (INSTANCE == null)
synchronized (NotifyManager.class)
{
if (INSTANCE == null)
INSTANCE = new NotifyManager(context);
}
return INSTANCE;
}
private NotificationManager manager;
// NotificationManagerCompat
private NotificationCompat.Builder builder;
// 初始化通知栏配置
private void initNotifyManager(Context context)
{
context = context.getApplicationContext();
manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// // 如果存在则清除上一个消息
// manager.cancel(lastNotificationId);
builder = new NotificationCompat.Builder(context, MyNotification.CHANNEL_ID_GL);
MyNotification.setONotifyChannel(manager, builder, MyNotification.CHANNEL_ID_GL, MyNotification.CHANNEL_NAME_GL);
// 设置标题
builder.setContentTitle("Title");
// 状态栏的动画提醒语句
builder.setTicker("Ticker");
// 什么时候提醒
builder.setWhen(System.currentTimeMillis());
// 设置通知栏的优先级
builder.setPriority(Notification.PRIORITY_DEFAULT);
// 设置点击可消失
builder.setAutoCancel(true);
// 设置是否震动等
builder.setDefaults(Notification.DEFAULT_VIBRATE);
// 设置icon
builder.setSmallIcon(R.mipmap.ic_logo);
// 设置点击意图
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
}
/**
* 显示盖伦通知栏
*
* @author Jack Zhang
* create at 2019/1/2 3:28 PM
*/
public void showGLNotify(Context context)
{
// 设置内容
builder.setContentText("盖伦");
manager.notify(1, builder.build());
}
/**
* 显示艾希通知栏
*
* @author Jack Zhang
* create at 2019/1/2 3:28 PM
*/
public void showAXNotify(Context context)
{
builder.setContentText("艾希");
manager.notify(2, builder.build());
}
/**
* 显示流浪通知栏
*
* @author Jack Zhang
* create at 2019/1/2 3:28 PM
*/
public void showLLNotify(Context context)
{
builder.setContentText("流浪");
manager.notify(3, builder.build());
}
}
SecurityException的闪退
问题原因:项目使用了ActiveAndroid,在 8.0 或 8.1 系统上使用 26 或以上的版本的 SDK 时,调用 ContentResolver 的 notifyChange 方法通知数据更新,或者调用 ContentResolver 的 registerContentObserver 方法监听数据变化时,会出现上述异常。
解决方案:
- 在清单文件配置:
<provider
android:name="com.activeandroid.content.ContentProvider"
android:authorities="com.jz.androidclient"
android:enabled="true"
android:exported="false"/>
- 去掉这个监听刷新的方法,改为广播刷新
静态广播无法正常接收
问题原因:Android 8.0 引入了新的广播接收器限制,因此您应该移除所有为隐式广播 Intent 注册的广播接收器。
解决方案:使用动态广播代替静态广播。
Only fullscreen opaque activities can request orientation
Caused by: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
问题原因:Android 8.0 非全屏透明页面不允许设置方向(后面8.1系统谷歌就去掉了这个限制,可能是真的没必要)
解决方案:
- android:windowIsTranslucent设置为false。
- 如果还是想用的话,就去掉清单文件中Activity中的android:screenOrientation="portrait"。
- 使用透明的dialog或者PopupWindow来代替,也可以用DialogFragment,看自己的需求和喜好。
Android 9.0适配
CLEARTEXT communication to life.115.com not permitted by network security policy
CLEARTEXT communication to life.115.com not permitted by network security polic
问题原因: Android P 限制了明文流量的网络请求,非加密的流量请求都会被系统禁止掉
解决方案:
- 在资源文件新建xml目录,新建文件network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
- 清单文件配置:
<application
android:networkSecurityConfig="@xml/network_security_config">
<!--Android 9.0加的-->
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
</application>
其他Api的修改
java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed
if (Build.VERSION.SDK_INT >= 26)
canvas.clipPath(mPath);
else
canvas.clipPath(mPath, Region.Op.REPLACE);