锁屏界面开发中遇到的各种坑
背景
android自4.0版本,也就是API level 14开始,加入了锁屏控制的功能,相关的类是RemoteControlClient,这个类在API level 21中被标记为deprecated,被新的类MediaSession所替代。我们的音乐App中最开始使用的是原生锁屏控制API,说实话这个API不好用,遇到了一些小坑,最要命的是不同品牌的手机,锁屏界面长的还不一样,就连我自己都没见过原生4.0的锁屏控制界面是什么样的。国内的手机厂商都自以为自己的审美很强,设计了千奇百怪的锁屏控制界面,MIUI更奇怪,MIUI 6是在原生4.4.4的基础上改的,竟然有一段时间都没有锁屏控制界面,后来更新才有。而原生Android在5.0时,将锁屏和通知栏控制合并,整个逻辑非常混乱。我们还是决定像网易云音乐/QQ音乐那样,自己做一个锁屏控制页面。
解决方案
类似网易云音乐和QQ音乐,一般是注册一个广播监听ACTION_SCREEN_OFF/ACTION_SCREEN_ON操作,然后启动一个activity。 基本代码如下:
private void addScreenChangeBroadCast() {
if(mScreenBroadcastReceiver == null){
mScreenBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
disableSystemLockScreen(context);
Logger.d(TAG, "Intent.ACTION_SCREEN_OFF");
Intent lockscreenIntent = new Intent();
lockscreenIntent.setAction(LOCKSCREEN_ACTION);
lockscreenIntent.setPackage(APP_PACKAGE);
lockscreenIntent.putExtra("INTENT_ACTION", action);
lockscreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(lockscreenIntent);
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
try {
registerReceiver(mScreenBroadcastReceiver, filter);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void removeScreenChangeBroadCast() {
if(mScreenBroadcastReceiver != null) {
try {
unregisterReceiver(mScreenBroadcastReceiver);
} catch (Exception e) {
e.printStackTrace();
}
mScreenBroadcastReceiver = null;
}
}
public static void disableSystemLockScreen(Context context) {
// 下面代码会出现某些系统home键启动后失效的问题
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
try {
KeyguardManager keyGuardService = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
KeyguardManager.KeyguardLock keyGuardLock = keyGuardService.newKeyguardLock("");
keyGuardLock.disableKeyguard();
} catch (Exception e) {
Logger.e(TAG, "disableSystemLockScreen exception, cause: " + e.getCause()
+ ", message: " + e.getMessage());
}
}
}
复制代码
Manifest如下:
<activity
android:name="com.activity.LockScreenActivity"
android:excludeFromRecents="true"
android:exported="false"
android:noHistory="true"
android:showOnLockScreen="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:taskAffinity="com.activity.LockScreenActivity"
android:hardwareAccelerated="true"
android:resizeableActivity="false"
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize"
android:theme="@style/LockScreenTheme">
<intent-filter>
<action android:name="com.android.lockscreen" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
复制代码
Activity在onCreate中需要添加在锁屏上显示的flag,在onBackPress不响应Back按键:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
Logger.d(TAG, getClass().getSimpleName() + ": onCreate");
super.onCreate(savedInstanceState);
Window window = getWindow();
if (window != null) {
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
fitStatusBar(window);
NavigationUtil.hideNavigationBar(window, true);
}
mSlideView = new SlideView(this, SlideView.TYPE_FRAMELAYOUT, R.color.framework_transparent);
mSlideView.setFullSlideAble(true);
LayoutInflater.from(this).inflate(R.layout.host_act_lockscreen, mSlideView.getContentView(), true);
setContentView(mSlideView);
initUi();
}
@Override
public void onBackPressed() {
// 锁屏界面当然不响应Back按键, 只需要重写Activity的onBackPressed方法即可
// super.onBackPressed();
}
复制代码
然后这就开始了我的填坑之路,下面这些经验不过是给大神取乐,给后来者抛砖引玉,不要在背后骂我就好。
填坑
1. 通过setAction startActivity 页面起不来
之前因为页面在上层,启动操作放在sdk层,因此只能通过setAction的启动方式,之前一直好好的然后到回归突然发现页面起不来了,经过大牛hook ActivtyThread代码发现LAUNCH_ACTIVITY确实被调用了,但是页面确实没起来说明页面没找到。最后通过对intent setPackage进行限制,然后一切都正常了。
2. 在某些4.x手机上会出现点Home键无法回到主界面的问题
遇到这种问题头皮都麻了,那就只能百度了。幸好找到这么个贴子,搞定。下面说下原因:
KeyguardManager的内部类KeyguardLock,它有两个方法用来禁用 disableKeyguard
和启用reenableKeyguard
屏保
但是禁用disable方法并不是解锁屏幕,只是把锁屏功能禁掉了,这也导致了今天要说的这个问题,在某些系统上锁屏界面仍然存在而且并没有解锁,导致按Home键的时候Home的实际功能被锁屏界面拦截而无法进入主页。而且调用完disable这个方法后,除非应用进程被杀死,否则按电源键只是黑屏,无法锁住屏幕的。
其次,KeyguardLock对象必须是同一个才能在disable之后重新reenable,所以要使reenable生效的话要把调用disable的对象存起来便于再reenable,而且单纯的调用reenable方法是没有任何作用的,所以你锁不了其他程序打开的屏幕,有时候甚至锁不了自己曾经打开的锁(对象不是同一个的话)
所以说来,这个disableKeyguard——屏蔽屏保的方法还是不能随意乱用啊,所以干脆把这部分代码去掉,问题就完美解决了!
3. 锁屏页面闪烁问题
锁屏页面展示后,关闭电源键稍等一下,再次打开,页面会发生闪烁现象。打印了一下LockScreenActivity的生命周期发现activty一遍不落的从onCreate到onDestroy执行了一遍,为什么会发生这种现象,这可是结合了众多的帖子整出来的代码,看着网易云音乐不会出现这种情况,好吧,那就开始一行行的代码删除吧,然后发现字段就出在noHistory上了,noHistory表明activty在用户不可见的时候即会执行finish,在statck中不留历史痕迹。一般用于空壳activty做跳转使用。所以在这个熄屏的过程中,页面就这样被销毁了。关于noHistory可以参考这个贴子。
4. 锁屏加载有时很缓慢
在自己的小米手机上和网易云音乐做对比,大部分情况都是网易云先出现,然后自己的锁屏页面姗姗来迟,有时还出现出不来的情况。另外app首次启动,第一次锁屏基本是起不来的,网易云音乐也有这样的情况。
首先把界面换成只有TextView结果依旧,然后打印每部分代码的运行时间,惊奇的发现startActivity每次都要大概3s左右才执行到onCreate,难道系统找个activity这么慢吗,结合1的问题,setPackage还是无效,帖子里有建议添加android:showWhenLocked="true"
加后发现确实变快了,但下午又变慢了。好吧,既然帖子没用,那就只能回本溯源,看看startActivity源码了,然后发现这么代码:
boolean checkAppSwitchAllowedLocked(int sourcePid, int sourceUid,
int callingPid, int callingUid, String name) {
if (mAppSwitchesAllowedTime < SystemClock.uptimeMillis()) {
return true;
}
int perm = checkComponentPermission(
android.Manifest.permission.STOP_APP_SWITCHES, sourcePid,
sourceUid, -1, true);
if (perm == PackageManager.PERMISSION_GRANTED) {
return true;
}
// If the actual IPC caller is different from the logical source, then
// also see if they are allowed to control app switches.
if (callingUid != -1 && callingUid != sourceUid) {
perm = checkComponentPermission(
android.Manifest.permission.STOP_APP_SWITCHES, callingPid,
callingUid, -1, true);
if (perm == PackageManager.PERMISSION_GRANTED) {
return true;
}
}
Slog.w(TAG, name + " request from " + sourceUid + " stopped");
return false;
}
复制代码
@Override
public void stopAppSwitches() {
if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("viewquires permission "
+ android.Manifest.permission.STOP_APP_SWITCHES);
}
synchronized(this) {
// static final long APP_SWITCH_DELAY_TIME = 5*1000;
// 这里设置的是5s 也就是在5s内是不允许app切换
mAppSwitchesAllowedTime = SystemClock.uptimeMillis()
+ APP_SWITCH_DELAY_TIME;
mDidAppSwitch = false;
mHandler.removeMessages(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
Message msg = mHandler.obtainMessage(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
mHandler.sendMessageDelayed(msg, APP_SWITCH_DELAY_TIME);
}
}
复制代码
所以一切都清楚了,像来电显示、闹钟这种系统应用是通过设置android.Manifest.permission.STOP_APP_SWITCHES
权限来响应后台activity启动,而普通应用只能耐心的等待了。把网易云音乐的包反编译看了下,普通的startActivity加上一堆flag,全部按照网易云的设置整了个遍还是那样,感觉可能是插件包和生产包的差异原因吧,因为线上的包感觉速度还可以,可能是做过混淆的缘故吧,jekins打了若干次包还是没用。最后想到像QQ的通知界面是通过PendingIntent启动展示的,在自己代码里试了下,问题就这样解决了,绕开了activity使用PendingIntent。
Intent intent = new Intent(context, LockScreenActivity.class);
intent.setPackage(APP_PACKAGE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_FROM_BACKGROUND
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
| Intent.FLAG_ACTIVITY_NO_ANIMATION
| Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
PendingIntent pendingIntent =
PendingIntent.getActivity(context, 0, intent, 0);
Logger.d(TAG, "pendingIntent.send() " + System.currentTimeMillis());
pendingIntent.send();
复制代码
就这样我做的锁屏页面赛过了网易云音乐,其中还有两点心得:
- APP启动注册广播的先后顺序会影响页面展示的先后,仔细想下就知道,System拿着mListeners肯定是按照谁先注册就先通知谁
- 因为SCREEN_ON是后于SCREEN_OFF的,所以如果SCREEN_ON和SCREEN_OFF都启动锁屏页面的话,只能按照SCREEN_ON来计算时间,因为SCREEN_OFF启动的页面会hold住,而SCREEN_ON启动的页面会随后hold住同时将之前hold的索引删除(也就是mPendingActivityLaunches)。如果要想APP锁屏页面启动更快,就不能在SCREEN_ON中启动activity
- 还有一种讨巧的办法不需要经历hold过程,可以参考咕咚app的锁屏页面。原理是监听SCREEN_OFF,然后把主activity移到前台,这样的startActivity就不是后台行为了,不过这样的用户体验会很差。
5. Android Q 锁屏适配
在Android Q中,Google这样解释到:
Android Q 对应用可启动 Activity的时间施加了限制。此项行为变更有助于最大限度地减少对用户造成的中断,并且可以让用户更好地控制其屏幕上显示的内容。具体而言,在 Android Q 上运行的应用只有在满足以下一个或多个条件时才能启动 Activity:
该应用具有可见窗口,例如在前台运行的 Activity。
在前台运行的另一个应用会发送属于该应用的 PendingIntent。示例包括发送菜单项待定 intent 的自定义标签页提供程序。
系统发送属于该应用的 PendingIntent,例如点按通知。只有应用应启动界面的待定 intent 才可以免除。
系统向应用发送广播,例如 SECRET_CODE_ACTION。只有应用应启动界面的特定广播才可以免除。
看到这些,不禁仰天长叹,赢了网易云音乐又如何,却输给了这个时代啊。
结语
好了,就写这么多吧,我要去呼吸新鲜空气了!
留下我的WX,欢迎各位大神点评。
参考资料
wossoneri.github.io/2018/06/03/…
stackoverflow.com/questions/5…