Exception is:java.lang.IllegalStateException: should not be called from the main thread
文章目录
1. Description
- 问题描述:
- 对设备进行加密设置
- 为设备设置pin密码
- 进入setting 进行系统升级,系统要求输入设置的密码并进行验证
- 输入正确密码,系统依然会提示密码错误
- 问题描述:
- 对设备进行加密设置
- 为设备设置pattern密码
- 进入setting 进行系统升级,系统要求输入设置的密码并进行验证
- 会要求用户输入text类型密码
操作步骤:
问题一(reproduction procedure):
- encryption the terminal
- set secreen lock to pin with “secure start-up” to YES
- copy PFU file to “/storage/emulated/0/”
- select PFU file in system update
- the pin input dialer will pop up
- enter pin and select ok
- the warning dialer of a wrong password will pop up
问题二(reproduction procedure):
- encryption the terminal
- set secreen lock to pattern with “secure start-up” to YES
- copy PFU file to “/storage/emulated/0/”
- select PFU file in system update
- the password input dialer will pop up
结果如下图:
2. Analysis
问题一:
- 运行时抓取到log发现在进行密码判断时 会抛出异常Exception is:java.lang.IllegalStateException: should not be called from the main thread
11-14 12:16:45.057 8922 8922 D SdcardUpgradeAct:the pwd2 is:1234
11-14 12:16:45.057 8922 8922 D SdcardUpgradeAct: the userId is:0
11-14 12:16:45.058 8922 8922 D SdcardUpgradeAct: the verifyUserPassword Exception is:java.lang.IllegalStateException: should not be called from the main thread.
- 然后查看到问题根源是在升级校验的apk中调用了framework层的
checkPassword
方法验证 用户输入的密码,而该方法是不能直接在主线程中使用的,否则throwIfCalledOnMainThread
就会主动的抛出异常中断校验过程。所以即使输入的是正确的密码,该apk也会提示password不正确.
/**
* Check to see if a password matches the saved password. If no password exists,
* always returns true.
* @param password The password to check.
* @return Whether the password matches the stored one.
*/
public boolean checkPassword(String password, int userId) throws RequestThrottledException {
return checkPassword(password, userId, null /* progressCallback */);
}
/**
* Check to see if a password matches the saved password. If no password exists,
* always returns true.
* @param password The password to check.
* @return Whether the password matches the stored one.
*/
public boolean checkPassword(String password, int userId,
@Nullable CheckCredentialProgressCallback progressCallback)
throws RequestThrottledException {
throwIfCalledOnMainThread();/*该方法会检查当前线程是否是主线程,如果是则会抛出异常*/
return checkCredential(password, CREDENTIAL_TYPE_PASSWORD, userId, progressCallback);
}
- 根据错误原因,解法也非常简单,可以将验证密码这个逻辑单独开一个线程来做,然后根据返回的结果来判断是继续进行系统升级还是做其他操作
问题二:
- 这个问题出现的原因是在这个apk 中根本就没有区分加密模式的逻辑,全都使用的是text来验证用户输入的密码,如果用户输入的密码等于当前userId在设备中存储的密码才通过验证。
- pin/password这两种都是text类型的密码当然可以通用,但是对于pattern则不行(虽然最终pattern密码也是以text类型存在data分区)。
- 所以需要在原来的基础上添加一个对pattern密码验证的功能。
- 默认情况下android 有三种加密模式,分别是 pin/password/pattern ,通过LockPatternUtils.java提供的getKeyguardStoredPasswordQuality方法可以获取当前用户使用的哪种加密模式。
- 如果getKeyguardStoredPasswordQuality方法返回的是DevicePolicyManager.PASSWORD_QUALITY_SOMETHING则说明是在pattern模式下。
- 然后可以使用LockPatternUtils.checkPattern来验证输入的pattern密码是否与当前用户存储的pattern密码一致。
需要注意的是checkPattern方法要求的输入参数是List<LockPatternView.Cell>
类型,具体如下:
/**
* 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)
throws RequestThrottledException {
return checkPattern(pattern, userId, null /* progressCallback */);
}
3. solution
**问题一大概的流程是: **
- 点击验证后 开线程去验证密码(耗时操作) ,根据验证结果向主线程发送相关信息
- 如果验证通过 让handle通知主线程 显示成功界面 升级操作需放在handle主体中做
- 如果验证失败 让handle通知主线程 显示验证失败界面 不做升级操作
public class SdcardUpgradeActivity extends PreferenceActivity implements OnClickListener,
ActivityCompat.OnRequestPermissionsResultCallback {
/*此处省略*/
/*modify for add pattern password verify begin*/
private boolean verifyUserPassword(String pwd) {
final int userId = getUserId();
boolean result = false;
try {
result = lockPatternUtils.checkPassword(pwd, userId);
} catch (Exception e) {}
return result;
}
private void checkEncryptAndInstall(){
DevicePolicyManager dpm = DevicePolicyManager.create(this);
int encryptionStatus = dpm.getStorageEncryptionStatus();
if(DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE == encryptionStatus){
lockPatternUtils=new LockPatternUtils(this);
/*if indicating that encryption is active ,get currnt userid's password quality*/
int type=lockPatternUtils.getKeyguardStoredPasswordQuality(getUserId());
Log.d(TAG, "current password quality is :"+type);
if(PASSWORD_QUALITY_SOMETHING==type){
Log.d(TAG, "current password quality is pattern! use patternLayout");
/*user pattern to verify password*/
startPatternVerifyActivity();
}else{
/*user pin/password to verify password*/
Log.d(TAG, "current password quality is pin/password! use PassWordLayout");
createTextPassWordLayout();
}
}else{
userPassword = null;
prepareInstall(true);
}
}
/*modify for add pattern password verify end*/
/*add for SdcardUpgrade to verify password without main thread restrict */
final Handler myHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
if(msg.what==0x123){
prepareInstall(true);/*the result is return, and verify is ok ,prepare Install*/;
}else if(msg.what==0x124){/*the result is return, and verify is not ok, show error message*/
mUtil.showDialog(mUtil.getStr(R.string.popup_dialog_title_attention),
mUtil.getStr(R.string.popup_text_input_correct_password),
mUtil.getStr(R.string.ok),null,null,null, null, null);
}
}
};
/*add for SdcardUpgrade to verify password without main thread restrict */
private void checkEncryptAndInstall(){
DevicePolicyManager dpm = DevicePolicyManager.create(this);
int encryptionStatus = dpm.getStorageEncryptionStatus();
if(DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE == encryptionStatus){
/*绘制密码输入界面,感觉根据单一原则可以封装为一个单独方法*/
LinearLayout pwdLayout = new LinearLayout(this);
pwdLayout.setOrientation(LinearLayout.VERTICAL);
float density = getResources().getDisplayMetrics().density;
pwdLayout.setPaddingRelative((int) (15 * density), (int) (10 * density), (int) (15 * density), (int) (10 * density));
TextView textView = new TextView(this);
textView.setText(mUtil.getStr(R.string.popup_dialog_desc_input_passwrod));
textView.setTextSize(16);
pwdLayout.addView(textView);
final EditText editText = new EditText(this);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
editText.setTextSize(16);
editText.setBackgroundColor(0xffF5DEB3);
pwdLayout.addView(editText);
new AlertDialog.Builder(this, android.R.style.Theme_Material_Light_Dialog_Alert)
.setTitle(mUtil.getStr(R.string.popup_dialog_title_input_passwrod))
.setView(pwdLayout)
.setPositiveButton(mUtil.getStr(R.string.ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
userPassword = editText.getText().toString().trim();
userPassword = userPassword.equals("") ? null : userPassword;
/*modify for SdcardUpgrade to verify password without main thread restrict */
if(userPassword != null){
new Thread(new Runnable() {
@Override
public void run() {
if(verifyUserPassword(userPassword)){
myHandler.sendEmptyMessage(0x123);}
else{
myHandler.sendEmptyMessage(0x124);
}
}
}).start();
/*modify for SdcardUpgrade to verify password without main thread restrict*/
}else {
mUtil.showDialog(mUtil.getStr(R.string.popup_dialog_title_attention),
mUtil.getStr(R.string.popup_text_input_correct_password),
mUtil.getStr(R.string.ok),null,null,null, null, null);
}
}
})
.setNegativeButton(mUtil.getStr(R.string.cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
userPassword = null;
}
}).create().show();
}else{
userPassword = null;
prepareInstall(true);
}
}
/*此处省略*/
}
**问题二大概的流程是: **
- 对当前用户使用的密码模式进行判断。
- 如果是pattern模式,则跳转到一个activity中,在该activity中绘制九宫格并让用户输入验证的密码。
- 在用户输入完后,将结果返回到前一个界面然后对用户输入的pattern进行验证。
- 和问题一样,根据返回结果通过handler作出不同的响应。
九宫格其实本质就是9个数字而已,然后根据用户点击的先后顺序来组合不同的密码,如下图:
获取到的密码是12357,所以在接收用户输入pattern的界面,可以将获取到的输入pattern以字符串12357的模式返回给上层,而在LockPatternUtils类中同时也提供了将string
转为List<LockPatternView.Cell>
的方法stringToPattern,具体如下:
/**
* Deserialize a pattern.
* @param string The pattern serialized with {@link #patternToString}
* @return The pattern.
*/
public static List<LockPatternView.Cell> stringToPattern(String string) {
if (string == null) {
return null;
}
List<LockPatternView.Cell> result = Lists.newArrayList();
final byte[] bytes = string.getBytes();
for (int i = 0; i < bytes.length; i++) {
byte b = (byte) (bytes[i] - '1');
result.add(LockPatternView.Cell.of(b / 3, b % 3));
}
return result;
}
然后在源代码中的修改如下:
public class SdcardUpgradeActivity extends PreferenceActivity implements OnClickListener,
ActivityCompat.OnRequestPermissionsResultCallback {
/*此处省略*/
/*modify for add pattern password verify begin*/
private boolean verifyUserPassword(String pwd) {
final int userId = getUserId();
boolean result = false;
try {
result = lockPatternUtils.checkPassword(pwd, userId);
} catch (Exception e) {}
return result;
}
private void checkEncryptAndInstall(){
DevicePolicyManager dpm = DevicePolicyManager.create(this);
int encryptionStatus = dpm.getStorageEncryptionStatus();
if(DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE == encryptionStatus){
lockPatternUtils=new LockPatternUtils(this);
/*if indicating that encryption is active ,get currnt userid's password quality*/
int type=lockPatternUtils.getKeyguardStoredPasswordQuality(getUserId());
Log.d(TAG, "current password quality is :"+type);
if(PASSWORD_QUALITY_SOMETHING==type){
Log.d(TAG, "current password quality is pattern! use patternLayout");
/*user pattern to verify password*/
startPatternVerifyActivity();
}else{
/*user pin/password to verify password*/
Log.d(TAG, "current password quality is pin/password! use PassWordLayout");
createTextPassWordLayout();
}
}else{
userPassword = null;
prepareInstall(true);
}
}
/*modify for add pattern password verify begin*/
/*add for add pattern password verify begin*/
private void createTextPassWordLayout(){
LinearLayout pwdLayout = new LinearLayout(this);
pwdLayout.setOrientation(LinearLayout.VERTICAL);
float density = getResources().getDisplayMetrics().density;
pwdLayout.setPaddingRelative((int) (15 * density), (int) (10 * density), (int) (15 * density), (int) (10 * density));
TextView textView = new TextView(this);
textView.setText(mUtil.getStr(R.string.popup_dialog_desc_input_passwrod));
textView.setTextSize(16);
pwdLayout.addView(textView);
final EditText editText = new EditText(this);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
editText.setTextSize(16);
editText.setTextColor(Color.BLACK);
pwdLayout.addView(editText);
new AlertDialog.Builder(this, android.R.style.Theme_Material_Light_Dialog_Alert)
.setTitle(mUtil.getStr(R.string.popup_dialog_title_input_passwrod))
.setView(pwdLayout)
.setPositiveButton(mUtil.getStr(R.string.ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
userPassword = editText.getText().toString().trim();
userPassword = userPassword.equals("") ? null : userPassword;
if(userPassword != null){
new Thread(new Runnable() {
@Override
public void run() {
if(verifyUserPassword(userPassword)){
myHandler.sendEmptyMessage(0x123);}
else{
myHandler.sendEmptyMessage(0x124);
}
}
}).start();
}else {
mUtil.showDialog(mUtil.getStr(R.string.popup_dialog_title_attention),
mUtil.getStr(R.string.popup_text_input_correct_password),
mUtil.getStr(R.string.ok),null,null,null, null, null);
}
}
})
.setNegativeButton(mUtil.getStr(R.string.cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
userPassword = null;
}
}).create().show();
}
private void startPatternVerifyActivity(){
Intent intent=new Intent();
intent.setClass(this,PatternVerifyActivity.class);/*PatternVerifyActivity是一个绘制九宫格的Activity,当用户输入完毕后,会将pattern结果以string 的形式返回回来*/
try {
startActivityForResult(intent,1);
}catch (Exception e)
{
Log.d(TAG, "start PatternVerifyActivity Activity encunter exception:"+e);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode){
case 1:
Log.d(TAG, "PatternVerifyActivity Activity return!");
if(1==resultCode)
{
String patternWord=data.getStringExtra(PatternVerifyActivity.PatternWord);
Log.d(TAG, "PatternVerifyActivity Activity return! patternWord is:"+patternWord);
verifyUserPayttern(patternWord);
}
break;
case 2:
break;
}
}
private void verifyUserPayttern(String patternWord)
{
List<LockPatternView.Cell> result=lockPatternUtils.stringToPattern(patternWord);
new Thread(new Runnable() {
@Override
public void run() {
try {
if(lockPatternUtils.checkPattern(result,getUserId())){
myHandler.sendEmptyMessage(0x123);}
else{
myHandler.sendEmptyMessage(0x124);
}
} catch (Exception e) {
Log.d(TAG, "Exception is:"+e);
}
}
}).start();
}
/*add for add pattern password verify*/
/*此处省略*/
}
然后绘制九宫格的方案网上有很多,此处参考轻松实现Android自定义九宫格图案解锁博客的简单做法,绘制一个九宫格然后记录用户的输入pattern并以string返回SdcardUpgradeActivity处理。
4. summary
这个问题引起的主要原因应该是后续framework更新导致,更新加强了对密码验证接口使用的条件。同时,验证密码部分的逻辑不够严谨,将耗时的操作放在了主线程中,这样也很容易发生anr问题。所有解法也简单,就是将这个操作拿出来,单开一个线程做就可以了。
而第二个问题就是添加一个pattern模式的验证就可以解决的,最开始做的时候,本来还想调用setting下面的绘制九宫格的逻辑,将整个绘制、验证的逻辑外包给setting来做,但是最后没有找到相关的接口 ,然后看网上很多九宫格绘制的方案,里面的逻辑也不复杂,并且主要的是android中已经有了将string
转化为List<LockPatternView.Cell>
的接口,解决了从用户处获取到的pattern密码与系统存储的pattern密码格式不匹配的问题。