锁屏界面开发中遇到的各种坑

锁屏界面开发中遇到的各种坑

背景

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();
复制代码

就这样我做的锁屏页面赛过了网易云音乐,其中还有两点心得:

  1. APP启动注册广播的先后顺序会影响页面展示的先后,仔细想下就知道,System拿着mListeners肯定是按照谁先注册就先通知谁
  1. 因为SCREEN_ON是后于SCREEN_OFF的,所以如果SCREEN_ON和SCREEN_OFF都启动锁屏页面的话,只能按照SCREEN_ON来计算时间,因为SCREEN_OFF启动的页面会hold住,而SCREEN_ON启动的页面会随后hold住同时将之前hold的索引删除(也就是mPendingActivityLaunches)。如果要想APP锁屏页面启动更快,就不能在SCREEN_ON中启动activity
  1. 还有一种讨巧的办法不需要经历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,欢迎各位大神点评。

参考资料

gityuan.com/2016/03/12/…

wossoneri.github.io/2018/06/03/…

blog.csdn.net/ixiaobu/art…

stackoverflow.com/questions/5…

developer.android.com/preview/pri…

shoewann0402.github.io/2019/03/16/…

转载于:https://juejin.im/post/5cd4de59f265da039444c604

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值