Android8.1 SystemUI 之图案锁验证流程

Keyguard之滑动解锁流程一文中,我们已经分析过,不同的安全锁类型是在KeyguardSecurityContainer中使用getSecurityView根据不同的securityMode inflate出来,并添加到界面上的。那么本文我们就来以图案锁为例分析一下,安全锁解锁时的验证流程吧。

Screenshot_20181116-100802.png

图案解锁的滑动事件处理

我们知道,Pattern锁所使用的layout是case Pattern: return R.layout.keyguard_pattern_view;

<com.android.keyguard.KeyguardPatternView ...>

...
            <com.android.internal.widget.LockPatternView
                android:id="@+id/lockPatternView"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:layout_marginEnd="8dip"
                android:layout_marginBottom="4dip"
                android:layout_marginStart="8dip"
                android:layout_gravity="center_horizontal"
                android:gravity="center"
                android:clipChildren="false"
                android:clipToPadding="false" />

...
    </FrameLayout>

</com.android.keyguard.KeyguardPatternView>

那么图案解锁的滑动事件处理,就是在LockPatternView,源码位置是android/frameworks/base/core/java/com/android/internal/widget/LockPatternView.java,是一个系统公共控件,下面我们就分析一下这个view是如何处理触摸输入的。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mInputEnabled || !isEnabled()) {
            return false;
        }

        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                handleActionDown(event);
                return true;
            case MotionEvent.ACTION_UP:
                handleActionUp();
                return true;
            case MotionEvent.ACTION_MOVE:
                handleActionMove(event);
                return true;
            case MotionEvent.ACTION_CANCEL:
                if (mPatternInProgress) {
                    setPatternInProgress(false);
                    resetPattern();
                    notifyPatternCleared();
                }
                ...
                return true;
        }
        return false;
    }

不同的MotionEvent对应几个不同的handle方法处理,代码行数太多,我们这里大致总结如下

  • ACTION_DOWN(handleActionDown):根据触摸事件的坐标,使用算法detectAndAddHit(x, y)获取是否有命中的点,如果有,会调用addCellToPattern将命中的Cell添加到mPattern中,后即回调mOnPatternListener.onPatternStart()通知监听器,KeyguardPatternView实现并监听了OnPatternListener,做了清除安全提示内容的动作。另外计算需要重绘区域,并调用invalidate进行局部重绘。

  • ACTION_MOVE(handleActionMove):在这里 LockPatternView会对所有的历史坐标加当前事件坐标遍历for (int i = 0; i < historySize + 1; i++),获取命中点,另外如果ACTION_DOWN时没有获取到命中点,流程同上面的ACTION_UP,然后也会回调mOnPatternListener.onPatternStart()。最后会把所有motionevent对应的重绘区域进行union,并调用invalidate进行局部重绘。
    关于MotionEvent的历史坐标getHistoricalX,getHistoricalY的解释可以参考developer文档-->MotionEvent

关于历史坐标
为了效率,Android系统在处理ACTION_MOVE事件时会将连续的几个多触点移动事件打包到一个MotionEvent对象中。我们可以通过getX(int)和getY(int)来获得最近发生的一个触摸点事件的坐标,然后使用getHistorical(int,int)和getHistorical(int,int)来获得时间稍早的触点事件的坐标,二者是发生时间先后的关系。所以,我们应该先处理通过getHistoricalXX相关函数获得的事件信息,然后在处理当前的事件信息。

        for (int i = 0; i < historySize + 1; i++) {
            final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
            final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
        ...
        }
  • ACTION_UP(handleActionUp):如果mPattern不为空的话,会重置mPatternInProgress,取消动画,然后回调mOnPatternListener.onPatternDetected(final List<LockPatternView.Cell> pattern),这时候就开始图案解锁的验证了。

  • ACTION_CANCEL:重置pattern状态,回调mOnPatternListener.onPatternCleared()

关于ACTION_CANCEL的产生和事件回传你一定要知道的事
在当前控件(子控件)收到前驱事件(ACTION_MOVE或者ACTION_MOVE)后,它的父控件突然插手(interceptTouchEvent)截断事件的传递,这时当前控件就会收到ACTION_CANCEL,收到此事件后,不管子控件此时返回true或者false,都认为这一个动作已完成,不会再回传到父控件的OnTouchEvent中处理,同时后续事件,会通过dispatchEvent方法直接传送到父控件这里来处理。
那么有的朋友就要问了,不是说如果子控件的OnTouchEvent返回false,表明事件未被处理,是回传到父控件去处理的吗? 其实,只有ACTION_DOWN事件才可以被回传,ACTION_MOVE和ACTION_UP事件会跟随ACTION_DOWN事件,即ACTION_DOWN是哪个控件处理的,后续事件都传递到这里,不会上抛到父控件,ACTION_CANCEL也不能回传。
结论:ACTION_CANCEL事件是收到前驱事件后,后续事件被父控件拦截的情况下产生,onTouchEvent的事件回传到父控件只会发生在ACTION_DOWN事件中

图案解锁验证

src/com/android/keyguard/KeyguardPatternView.java

    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
    ...
        @Override
        public void onPatternDetected(final List<LockPatternView.Cell> pattern) {
            // 禁用事件输入,取消前面的AsyncTask
            mLockPatternView.disableInput();
            if (mPendingLockCheck != null) {
                mPendingLockCheck.cancel(false);
            }

            final int userId = KeyguardUpdateMonitor.getCurrentUser();
            // 如果连接的点小于4个,作为无效密码
            if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
                mLockPatternView.enableInput();
                onPatternChecked(userId, false, 0, false /* not valid - too short */);
                return;
            }

            mPendingLockCheck = LockPatternChecker.checkPattern(
                    mLockPatternUtils,
                    pattern,
                    userId,
                    new LockPatternChecker.OnCheckCallback() {...});
            if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
                mCallback.userActivity();
            }
        }
    ...
    }

checkPattern方法中构造了一个AsyncTask,并start()。
在onPreExecute()中复制了一份pattern,用ArrayList包装
patternCopy = new ArrayList(pattern);

关于AsyncTask的cancel(boolean mayInterruptIfRunning) 方法
先说结论,单独使用cancel不会像你想的那样好好工作
如果你调用了AsyncTask的cancel(false),doInBackground()仍然会执行到方法结束,只是不会去调用onPostExecute()方法。但是实际上这是让应用程序执行了没有意义的操作。
那么是不是我们调用cancel(true)前面的问题就能解决呢?并非如此。如果mayInterruptIfRunning设置为true,会使任务尽早结束,但是如果的doInBackground()有不可打断的方法会失效。
可见.cancel()是给AsyncTask设置一个"canceled"的状态,那么想要终止异步任务,就需要在异步任务当中结束。

// Task被取消了,马上退出
if(isCancelled()) return null;
.......
// Task被取消了,马上退出
if(isCancelled()) return null;
}

在doInBackground()中LockPatternUtils登场了,图案密码的验证,就是调用了LockPatternUtils的checkPattern方法
return utils.checkPattern(patternCopy, userId, callback::onEarlyMatched);

下面我们来看一下checkPattern方法的定义:
android/frameworks/base/core/java/com/android/internal/widget/LockPatternUtils.java

    /**
     * Check to see if a pattern matches the saved pattern.  If no pattern exists,
     * always returns true.
     * @param pattern The pattern to check.
     * @return Whether the pattern matches the stored one.
     */
    public boolean checkPattern(List<LockPatternView.Cell> pattern, int userId,
            @Nullable CheckCredentialProgressCallback progressCallback)
            throws RequestThrottledException {
        throwIfCalledOnMainThread();
        return checkCredential(patternToString(pattern), CREDENTIAL_TYPE_PATTERN, userId,
                progressCallback);
    }

LockPatternUtils首先会把pattern使用patternToString算法转换成字符串,之后调用checkCredential进行验证
在LockPatternUtils中还有一个checkPassword方法,对应的是PIN码/密码,所以图案锁/密码锁/PIN码锁的验证流程到这里开始就一致了。

    /**
     * Serialize a pattern.
     * @param pattern The pattern.
     * @return The pattern in string form.
     */
    public static String patternToString(List<LockPatternView.Cell> pattern) {
        if (pattern == null) {
            return "";
        }
        final int patternSize = pattern.size();

        byte[] res = new byte[patternSize];
        for (int i = 0; i < patternSize; i++) {
            LockPatternView.Cell cell = pattern.get(i);
            res[i] = (byte) (cell.getRow() * 3 + cell.getColumn() + '1');
        }
        return new String(res);
    }

KeyguardPINView/KeyguardPasswordView/KeyguardSimPinView/KeyguardSimPukView
都是继承自KeyguardAbsKeyInputView,用于密码验证的方法是verifyPasswordAndUnlock()
其中,KeyguardSimPinView和KeyguardSimPukView重写了该方法,所以有自己独特的验证方式。
而KeyguardPasswordView和KeyguardPINView都是使用LockPatternUtils的checkPassword进行密码验证

checkPassword和checkPassword都会去调用checkCredential方法

    private boolean checkCredential(String credential, int type, int userId,
            @Nullable CheckCredentialProgressCallback progressCallback)
            throws RequestThrottledException {
        try {
            VerifyCredentialResponse response = getLockSettings().checkCredential(credential, type,
                    userId, wrapCallback(progressCallback));

            if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
                return true;
            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
                throw new RequestThrottledException(response.getTimeout());
            } else {
                return false;
            }
        } catch (RemoteException re) {
            return false;
        }
    }

这里的getLockSettings()是什么呢?
ILockSettings service = ILockSettings.Stub.asInterface( ServiceManager.getService("lock_settings"));
原来是获得了LockSettingService的IPC接口,asInterface获取到aidl的Stub.Proxy对象,对本地数据进行封包,进行进程间通信。到这里我们明白了,原来密码的验证,最终是在LockSettingService中进行的。

android/frameworks/base/services/core/java/com/android/server/locksettings/LockSettingsService.java

    @Override
    public VerifyCredentialResponse checkCredential(String credential, int type, int userId,
            ICheckCredentialProgressCallback progressCallback) throws RemoteException {
        checkPasswordReadPermission(userId);
        VerifyCredentialResponse response = doVerifyCredential(credential, type, false, 0, userId, progressCallback);
        if ((response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) &&
                            (userId == UserHandle.USER_OWNER)) {
                retainPassword(credential);
        }
        return response;
    }

到这里, 我们的图案锁解锁流程就算是梳理清楚了。

总结一下:在绘制密码后手指抬起的时候,如果已存的有效点数达到4个及以上,就会使用LockPatternChecker.checkPattern方法启动一个AsyncTask, 并在doInBackground中调用LockPatternUtils.checkPattern进行密码验证,此时pattern会被转化成字符串形式,最终和密码锁PIN码锁一样,都是远程调用到LockPatternService的checkCredential接口进行验证。在整个操作过程中,mOnPatternListener被用于通知LockPatternView进行安全锁提示内容和Pattern状态的刷新。

checkCredential的具体算法逻辑以及LockPatternUtils和LockSettingsService中还有很多与安全锁加锁/解锁/锁状态判断相关的接口,后面我们会单独进行分析:)。

本文章已经独家授权ApeClub公众号使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343