此问题是草稿箱存了两年的一篇文章,还是重新发表了吧……^.^
当时新工作的第一个Bug,挺有纪念意义的,所以写下总结。

问题的现象:
1.打开 Settings → Security →Screen lock,设置PIN。
2.重新打开该选项,输入错误的PIN五次,手机会开始提示30s后才能继续尝试。
3.等待30s后,再次输入错误的PIN五次,观察现象。

预期结果:
步骤3之后的效果和步骤2之后的效果一样,需要30s之后才能重新尝试输入。

实际结果:
步骤3之后,点击continue按钮无反应,输入框里仍可以继续输入。

复现概率:
5/5

问题分析过程:

.
猜想是代码逻辑有问题,所以首先找到该页面的实现代码。
根据页面关键字串找到该页面的实现逻辑在:packages/apps/Settings/src/com/android/settings/ConfirmLockPassword.java类中:

二.
页面点击事件的处理如下:

        public void onClick(View v) {if (getActivity() == null) return;switch (v.getId()) {case R.id.next_button:  //点击CONTINUE按钮handleNext();break;case R.id.cancel_button:    //点击CANCEL按钮getActivity().setResult(RESULT_CANCELED);getActivity().finish();break;}}

其中handleNext()定义如下:

        private void handleNext() {final String pin = mPasswordEntry.getText().toString();if (mLockPatternUtils.checkPassword(pin)) { //输入正确的PIN之后的处理Intent intent = new Intent();if (getActivity() instanceof ConfirmLockPassword.InternalActivity) {intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE,mIsAlpha ? StorageManager.CRYPT_TYPE_PASSWORD: StorageManager.CRYPT_TYPE_PIN);intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pin);}getActivity().setResult(RESULT_OK, intent);getActivity().finish();} else {        //输入的PIN错误if (++mNumWrongConfirmAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {    //连续错误次数大于等于5次,则启动倒计时long deadline = mLockPatternUtils.setLockoutAttemptDeadline();handleAttemptLockout(deadline);} else {    //连续输入错误次数小于5次,则还可以继续尝试showError(R.string.lockpattern_need_to_unlock_wrong);}}}

而 handleAttemptLockout()定义如下:

private void handleAttemptLockout(long elapsedRealtimeDeadline) {if (mCountdownTimer != null) {return;}long elapsedRealtime = SystemClock.elapsedRealtime();   //获取系统当前时间showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, 0);mPasswordEntry.setEnabled(false);mCountdownTimer = new CountDownTimer(   elapsedRealtimeDeadline - elapsedRealtime,LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {@Overridepublic void onTick(long millisUntilFinished) {  //此处是30s倒计时的处理final int secondsCountdown = (int) (millisUntilFinished / 1000);mHeaderText.setText(getString(R.string.lockpattern_too_many_failed_confirmation_attempts_footer,secondsCountdown));}@Overridepublic void onFinish() {    //30s倒计时完成mPasswordEntry.setEnabled(true);mHeaderText.setText(getDefaultHeader());mNumWrongConfirmAttempts = 0;}}.start();
}

每当我们连续输入5次错误的PIN之后,程序就会进入这个方法进行处理。
那么第一次输入5次错误PIN的时候,进入此方法,首先会判断 mCountdownTimer是否为null。在此类中可以找到mCountdownTimer的声明:

        private CountDownTimer mCountdownTimer;

在这里mCountdownTimer默认初始化为null,其他地方并未初始化。所以mCountdownTimer != null判断失败,此方法继续执行。
接下来,创建了一个新的用于倒计时30s的对象,并开始倒计时。

倒计时结束后,输入框恢复为可输入状态。这时再输入错误的PIN五次后,点击CONTINUE按钮,流程又会进入 handleAttemptLockout()方法中。
此时由于mCountdownTimer对象已经被创建了,并不为null,所以直接执行了return()。
继续点击CONTINUE,又进入handleAttemptLockout()中,还是return。所以再点击CONTINUE都没反应了。

三.
知道问题发生的原因之后,我一开始改动想法是这样:
既然第二次输入错误5次之后,mCountdownTimer对象已经创建了,这时候要启动倒计时,只要直接让mCountdownTimer重新执行start()方法就好了。
于是改成如下进行测试:

            if (mCountdownTimer != null) {showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, 0);mPasswordEntry.setEnabled(false);mCountdownTimer.start();return;}

测试结果:第二次连续输入5次错误PIN之后,果然倒计时可以正常进行了。
但是经多次测试,发现有时候倒计时并不是30s,有时候从十几秒,有时候从二十几秒开始倒计时。这是什么原因呢?
经过打log追踪规律,终于发现:如果在倒计时未完成的过程中退出此页面,下次重新输入5次PIN错误之后,倒计时就会不正常。
原因如下:
倒计时过程中如果退出该页面,则会执行onPause()方法:

        @Overridepublic void onPause() {super.onPause();mKeyboardView.requestFocus();if (mCountdownTimer != null) {mCountdownTimer.cancel();mCountdownTimer = null;}}

可以知道,如果退出此页面的话, mCountdownTimer就会被置为null。但是要注意倒计时还在继续,cancel()方法并不会终止倒计时的进行。

而再次进入该页面的时候,会执行onResume()方法:

        @Overridepublic void onResume() {// TODO Auto-generated method stubsuper.onResume();mKeyboardView.requestFocus();long deadline = mLockPatternUtils.getLockoutAttemptDeadline();if (deadline != 0) {    //倒计时未完成handleAttemptLockout(deadline);} else {mPasswordEntry.setEnabled(true);mHeaderText.setText(getDefaultHeader());mNumWrongConfirmAttempts = 0;}}

可以看出,如果重新进入此页面的话,如果倒计时仍未完成,则又执行 handleAttemptLockout()方法,传入的参数 deadline即为倒计时截止时的时间。
此时由于mCountdownTimer已经在onPause()方法中被置为null,所以执行handleAttemptLockout()方法后会创建一个新的mCountdownTimer,而根据传入的构造参数看,这个mCountdownTimer开始倒计时的时间就是剩余的倒计时时间,至此流程都是正常的。

但是如果这次倒计时完成后,又接着输入五次错误的PIN码之后,由于上次创建的mCountdownTimer此时不为null,所以本次会直接执行此对象的start()方法开始倒计时。但是由于上次创建对象时传入的倒计时时间参数不是30s,所以这次倒计时也不会从30s开始倒计时,而是从和上次进入倒计时页面时的剩余时间一样的时间开始,问题就是这样出现了。

因此之前的改法存在问题,得另找方法。

四.
既然mCountdownTimer有时候会被置为null,有时候又会被创建新对象。不如每次倒计时完成都置为null,每次需要倒计时再创建新对象。
为实现此目的,把handleAttemptLockout()方法做一行改动:

            if (mCountdownTimer != null) {mCountdownTimer = null;
//                return;}

就是把原来的return改为了mCountdownTimer = null。
改动之后的方法如下:

        private void handleAttemptLockout(long elapsedRealtimeDeadline) {if (mCountdownTimer != null) {mCountdownTimer = null;}long elapsedRealtime = SystemClock.elapsedRealtime();showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, 0);mPasswordEntry.setEnabled(false);mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime,LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {@Overridepublic void onTick(long millisUntilFinished) {final int secondsCountdown = (int) (millisUntilFinished / 1000);mHeaderText.setText(getString(R.string.lockpattern_too_many_failed_confirmation_attempts_footer,secondsCountdown));}@Overridepublic void onFinish() {mPasswordEntry.setEnabled(true);mHeaderText.setText(getDefaultHeader());mNumWrongConfirmAttempts = 0;}}.start();}

改动之后,
对于handleAttemptLockout()方法,流程走到这个方法时,有两种可能,
1.上次倒计时创建的mCountdownTimer此时已经倒计时完成,继续输入5次错误的PIN后执行此方法,此时mCountdownTimer不为null。
2.上次倒计时创建的mCountdownTimer倒计时过程中,页面退出,重新打开此页面时onResume()方法中又执行了此方法,但mCountdownTimer在页面退出前onPause()方法中已被置为null。
对于第一种可能,mCountdownTimer会被置为null,然后创建一个新的mCountdownTimer开始倒计时。
对于第二种可能,流程和改动之前无差别,会直接创建新的mCountdownTimer开始倒计时。

五.
上面的改动,经测试是可以解决问题的。但是对流程影响较大,有潜在的风险。因此思考之后,进一步优化,改为如下方案:

        private void handleAttemptLockout(long elapsedRealtimeDeadline) {if (mCountdownTimer != null) {return();//这里不动}long elapsedRealtime = SystemClock.elapsedRealtime();showError(R.string.lockpattern_too_many_failed_confirmation_attempts_header, 0);mPasswordEntry.setEnabled(false);mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime,LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {@Overridepublic void onTick(long millisUntilFinished) {final int secondsCountdown = (int) (millisUntilFinished / 1000);mHeaderText.setText(getString(R.string.lockpattern_too_many_failed_confirmation_attempts_footer,secondsCountdown));}@Overridepublic void onFinish() {mCountdownTimer = null;//改到这里mPasswordEntry.setEnabled(true);mHeaderText.setText(getDefaultHeader());mNumWrongConfirmAttempts = 0;}}.start();}

即将mCountdownTimer = null放到了onFinish()方法中,使得每次倒计时完成时,mCountdownTimer会被置为null。

与上次的改动相比:上次的是在每次倒计时开始之前,将上次的mCountdownTimer置为null。而本次改动是将mCountdownTimer置为null的操作放在了mCountdownTimer倒计时完成之后。这样改动,流程更加合理清楚,风险也更得以避免。

总结:
1.改Bug的时候,一定先要理清流程,对于各种流程的可能性都要考虑到,必要的时候可以通过画流程图,分条列出等方式进行分析。
2.改动之后,充分验证,一般容易忽略的验证比如横竖屏切换,点击Back或者Home返回又重新进入,以及重复多次验证等都要经过测试。
3.在所有可行的方案中,选择对原有流程影响最小,最安全的。一次改动最好只解决一个问题。

一个Bug案例的解决过程:连续输入错误的PIN码,不能实现第二次倒计时30s才能重试相关推荐

  1. 怎么解决运行时输入错误,请重新输入以及专业无法输入的问题

    #include<iostream> using namespace std; #include<string> #define MAX 1000     // 设计联系人结构 ...

  2. oracle rac节点重启,oracle RAC一个节点频繁重启解决

    oracle RAC一个节点频繁重启解决 类别:Oracle数据库   作者:码皇   来源:hijk139的专栏     点击: oracle RAC一个节点频繁重启解决故障现象:2011年的一次问 ...

  3. MySQL数据库用户密码连续5次输入错误限定用户登录

    为数据库安全第三方会进行渗透测试,为防止恶意暴力破解用户密码,在用户登录时密码连续输入错误一定次数后限定用户的登录.本文通过插件实现当用户连续输入5次错误密码后显示其登录. 连接控制插件 MySQL数 ...

  4. 历经四个月,谷歌联盟的PIN码问题终于解决了

    历经四个月,谷歌联盟的PIN码问题终于解决了. 什么是PIN码?其实就是谷歌再给你报酬前的一次地址认证,以邮件的形式发给你,邮件里有一串六位数字就是PIN码.个人觉得这种邮件的方式效率太慢了. 谷歌官 ...

  5. c语言怎么同时输入两个字符,解决C语言中使用scanf连续输入两个字符类型的问题...

    昨天用C编程,遇到一个关于scanf的细节问题,假如运行如下程序: #include int main() { char ch1,ch2; printf("Input for ch1:/n& ...

  6. 生产中NFS案例记录---写入权限解决过程

        生产中NFS案例记录---写入权限解决过程 NFS配置要求: 1. 将oracle文件写入到NFS Server端,注意权限要与oracle端一致. 2. Oracle端目录文件所属用户为or ...

  7. linux mysql 死锁进程_一个罕见的MySQL redo死锁问题排查及解决过程

    作者:张青林,腾讯云布道师.MySQL架构师,隶属腾讯TEG-基础架构部-CDB内核开发团队,专注于MySQL内核研发&相关架构工作,有着服务多个10W级QPS客户的数据库优化及稳定性维护经验 ...

  8. MySQL 遇到的死锁问_一个罕见的MySQL redo死锁问题排查及解决过程

    原标题:一个罕见的MySQL redo死锁问题排查及解决过程 作者:张青林,腾讯云布道师.MySQL架构师,隶属腾讯TEG-基础架构部-CDB内核开发团队,专注于MySQL内核研发&相关架构工 ...

  9. 记录一次bug解决过程:数据迁移

    一 总结 不擅长语言表达,勤于沟通,多锻炼 调试MyBatis中SQL语法:foreach 问题:缺少关键字VALUES.很遗憾:它的错误报的让人找不着北. 二 BUG描述:MyBatis中批量插入数 ...

最新文章

  1. mysql5.1 与mysql5.5 字符集设置区别
  2. 解决pjax加载页面不执行js插件的问题
  3. 前谷歌工程师:如何看待程序员普遍缺乏数据结构和算法知识?
  4. 什么叫pmt测试分析_直读分析光谱仪核心配件
  5. boost::math模块使用逆高斯(或逆正态)分布的示例
  6. 学了C++不会STL,简直少了左膀右臂
  7. 好玩的脚本代码大全_Github | 推荐一个Python脚本集合项目
  8. Linux基础:find命令总结
  9. cytoscape使用方法_关于这种“网络模块”和“模块饼图”的可视化方法
  10. java开发安卓app_开发安卓app常用的三种开发语言
  11. Python检查批量URL是否可以正常访问
  12. nxp单片机入门_使用恩智浦MCUXpresso开发FRDM-KL46Z入门
  13. python小乌龟编程_Python案例——喝墨水的小乌龟
  14. 文件模式为 rw-r r linux,linux中-rw-rw-r-- l 是什么意思啊,linux 里命令ls -l 后,文件类型权...
  15. 【Leetcode 刷题题解】python语言+最优美解答+由易到难
  16. 修改IAR for msp430工程名方法
  17. [野史乱弹]对一段历史的大胆臆测与还原 [暴笑转载]
  18. java邮箱地址正则表达式_Java 正则表达式匹配邮箱地址
  19. Graylog日志简介
  20. 标题:互联网轻松赚钱之道。。

热门文章

  1. 千行百业中的我们,数字山河间的中国速度
  2. MySQL给查询结果添加序号列的书写格式
  3. nalu模式多slice_H.264中NAL、Slice与frame意思及相互关系
  4. c语言算正方形面积和周长,c语言中编写一程序计算正方形的周长和面积
  5. 用python判断闰年
  6. 用matlab画旋转抛物面_基于MATLAB的旋转抛物面天线的几种特性的仿真
  7. C语言中实现bool(布尔型变量)
  8. RGB-Infrared Cross-Modality Person Re-Identification---阅读
  9. Jenkins 登录忘记用户名和密码
  10. html文本通常由版本信息组成,第 2 章 网页版面设计.ppt