Settings 中 显示密码 选项是怎样工作的

How the 'SHOW_PASSWORD' option work ?

Posted by Roger on March 20, 2015

最近由于工作上的需要,研究了一下framework层面的东西。收获良多,感受颇深啊。

在 设置->安全 中选择屏幕锁定,选择屏幕锁定方式为密码,然后勾选显示密码选项,此时应该在输入密码时,先显示输入的密码,过1.5S后将变为小圆点,若取消显示密码则输入直接为小圆点。

而由于“前人”对锁屏的改动造成取消显示密码后,还是先显示密码才跳为小圆点,这是我要解决的BUG。

研究后发现“前人”将 Keyguard 中 com.android.keyguard.KeyguardPasswordView 类 106 行改变了Textview的inputtype,将其恢复后BUG得以解决。

Why?我一头雾水,甚至都不知道是在哪把输入的字符变成小圆点的!接下来就开始了我的求知之旅。

首先找到Settings中对应显示密码的代码。位于 com.android.settings.SecuritySettings ,找到575行

Settings.System.putInt(getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD,mShowPassword.isChecked() ? 1 : 0);

Settings.System.TEXT_SHOW_PASSWORD,这是存储按钮是否选中的key,于是在Keyguard工程中全局搜索,居然找不到~顿时语塞。

于是回到锁屏页面,发现输入框的EditView调用了一个 setKeyListener(TextKeyListener.getInstance()); 方法,来到 TextKeyListener 末尾看到两个方法

private void updatePrefs(ContentResolver resolver) {
         boolean cap = System.getInt(resolver, System.TEXT_AUTO_CAPS, 1) > 0;
         boolean text = System.getInt(resolver, System.TEXT_AUTO_REPLACE, 1) > 0;
         boolean period = System.getInt(resolver, System.TEXT_AUTO_PUNCTUATE, 1) > 0;
         boolean pw = System.getInt(resolver, System.TEXT_SHOW_PASSWORD, 1) > 0;

         mPrefs = (cap ? AUTO_CAP : 0) |
                  (text ? AUTO_TEXT : 0) |
                  (period ? AUTO_PERIOD : 0) |
                  (pw ? SHOW_PASSWORD : 0);}
/* package */
int getPrefs(Context context) {
         synchronized (this) {
                  if (!mPrefsInited || mResolver.get() == null) {
                           initPrefs(context);
                  }
         }
         return mPrefs;
}

可以看到 mPrefs 的值是会被 System.TEXT_SHOW_PASSWORD 影响的。看来只能到TextView源码中看了。在Textview源码中搜索 mPrefs 无果,再搜索 System.TEXT_SHOW_PASSWORD 还是无果。

最后只能在网站 http://androidxref.com/ 中搜索 mPrefs ,定位到类 PasswordTransformationMethod 中。看了老半天,结合好几个类,大概查出了点思路。

在setting中勾选显示密码后,Settings.System.TEXT_SHOW_PASSWORD 的值将变为1。

回到Textview中,若我们设置 android:inputType=”textPassword” 或在代码中设置 setInputType(InputType.TYPE_CLASS_TEXT InputType.TYPE_TEXT_VARIATION_PASSWORD);后

来到TextView的3987行

public void setInputType(int type) {
         final boolean wasPassword = isPasswordInputType(getInputType());
         final boolean wasVisiblePassword = isVisiblePasswordInputType(getInputType());
         setInputType(type, false);
         final boolean isPassword = isPasswordInputType(type);
         final boolean isVisiblePassword = isVisiblePasswordInputType(type);
         boolean forceUpdate = false;
         if (isPassword) {
                  setTransformationMethod(PasswordTransformationMethod.getInstance());
                  setTypefaceFromAttrs(null /* fontFamily */, MONOSPACE, 0);
         } else if (isVisiblePassword) {
                  if (mTransformation == PasswordTransformationMethod.getInstance()) {
                           forceUpdate = true;
                  }
                  setTypefaceFromAttrs(null /* fontFamily */, MONOSPACE, 0);
         } else if (wasPassword || wasVisiblePassword) {
// not in password mode, clean up typeface and transformation
                  setTypefaceFromAttrs(null /* fontFamily */, -1, -1);
                  if (mTransformation == PasswordTransformationMethod.getInstance()) {
                           forceUpdate = true;
                  }
         }

         boolean singleLine = !isMultilineInputType(type);

// We need to update the single line mode if it has changed or we
// were previously in password mode.
         if (mSingleLine != singleLine || forceUpdate) {
// Change single line mode, but only change the transformation if
// we are not in password mode.
                  applySingleLine(singleLine, !isPassword, true);
         }

         if (!isSuggestionsEnabled()) {
                  mText = removeSuggestionSpans(mText);
         }

         InputMethodManager imm = InputMethodManager.peekInstance();
                  if (imm != null) imm.restartInput(this);
}

将判断isPassword为true,于是设置 setTransformationMethod(PasswordTransformationMethod.getInstance()); 看到1702行setTransformationMethod方法,其实是将其赋给mTransformation这个变量。

重点来了,看到setText方法中,类的第3771行起

if (mTransformation == null) {
                  mTransformed = text;
         } else {
                  mTransformed = mTransformation.getTransformation(text, this);
         }

我们可以看到 mTransformation将输入的text转换了一遍,那么这个方法在PasswordTransformationMethod是怎样的呢,源码如下:

public CharSequence getTransformation(CharSequence source, View view) {
         if (source instanceof Spannable) {
                  Spannable sp = (Spannable) source;

         /*
         * Remove any references to other views that may still be
         * attached. This will happen when you flip the screen
         * while a password field is showing; there will still
         * be references to the old EditText in the text.
         */
                  ViewReference[] vr = sp.getSpans(0, sp.length(),
                  ViewReference.class);
                  for (int i = 0; i < vr.length; i++) {
                           sp.removeSpan(vr[i]);
                  }

                  removeVisibleSpans(sp);

                  sp.setSpan(new ViewReference(view), 0, 0,
                  Spannable.SPAN_POINT_POINT);
         }

         return new PasswordCharSequence(source);
}

最后返回了一个 PasswordCharSequence(source); 进一步跟踪PasswordCharSequence类,方法charAt源码:

public char charAt(int i) {
         if (mSource instanceof Spanned) {
                  Spanned sp = (Spanned) mSource;
                  int st = sp.getSpanStart(TextKeyListener.ACTIVE);
                  int en = sp.getSpanEnd(TextKeyListener.ACTIVE);
                  if (i >= st && i < en) {
                           return mSource.charAt(i);
                  }
                  Visible[] visible = sp.getSpans(0, sp.length(), Visible.class);
                  for (int a = 0; a < visible.length; a++) {
                           if (sp.getSpanStart(visible[a].mTransformer) >= 0) {
                                    st = sp.getSpanStart(visible[a]);
                                    en = sp.getSpanEnd(visible[a]);
                                    if (i >= st && i < en) {
                                             return mSource.charAt(i);
                                    }
                           }
                  }
         }
         return DOT;
}

发现了没!原来圆点是从这里来的!如果判断Textview的方式为Password时,在SetText方法中就通过PasswordTransformationMethod将其转换为小圆点。

那显示密码过1.5S后变为圆点又是如何实现的呢!?

看到 PasswordTransformationMethod 中的 onTextChanged 方法,重要代码如下:

int pref = TextKeyListener.getInstance().getPrefs(v.getContext());
         if ((pref & TextKeyListener.SHOW_PASSWORD) != 0) {
                  if (count > 0) {
                                    removeVisibleSpans(sp);
                                    if (count == 1) {
                                    sp.setSpan(new Visible(sp, this), start, start + count,
                                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                           }
                  }
         }

每次输入时都会调用onTextChanged 方法,并判断显示密码是否选中,若为选中状态,则 removeVisibleSpans ,将之前没有变成点的字符变成点,然后将Visible作为Span添加到末尾,我们来看下Visible是怎么写的

private static class Visible extends Handler
implements UpdateLayout, Runnable{
         public Visible(Spannable sp, PasswordTransformationMethod ptm) {
                  mText = sp;
                  mTransformer = ptm;
                  postAtTime(this, SystemClock.uptimeMillis() + 1500);
         }

         public void run() {
                  mText.removeSpan(this);
         }
         private Spannable mText;
         private PasswordTransformationMethod mTransformer;
}

一切如此明了,Visible将添加的字符显示出来,并在1.5S后将其remvoe,变回原来设置的小圆点!

至此,锁屏密码中“显示密码”选项是怎样工作的 已经能摸出个大概了。其中还有很多猜测和不了解深入的地方,还请大神多多拍砖指正。

第一次写解析文章,水平有限大家还请见谅~不过能看到这还是很感谢大家的耐心,如有什么问题请留言~人多力量大!