Friday, January 23, 2015

Extend screen unlock setting

General idea of the stuff I put into Android system: We want to add our own face recognition program to Android system as one of the screen unlock methods. Basically, there are two things we need to consider:
  1. Add face unlock entry to the Settings->Security->Screen lock menu as shown in fig. 1 (a).
  2. Add face unlock view to Android screen lock as shown in fig. 1 (b).
Face unlock settings
Fig1. (a) screen lock settings Fig. 1 (b) face unlock view
Before start, you'll need a Android system build tool chain, Android source code, Android SDK and an Android phone. Following is my spec. of development environment:
  • LG Nexus 4
  • AOSP: android-4.4.2_r2
  • Eclipse Luna with ADT plugin (Android API 19)
 OK, to figure it out how to extend Android system with the face recognition unlock, I spent a lot of time searching related articles. I don't think there are much articles and tutorials online that talk the whole process thoroughly. I did find people discussing related problems, but it's still hard to get the whole picture. Therefore, this article is all about how I integrated face unlock to android screen unlock code stacks.

Extend screen unlock settings

Add biometric entry to the unlock list in file “packages/apps/Settings/res/xml/security_settings_picker.xml” below "unlock_set_password" preference screen declaration


The definitions for the plain strings are in the "packages/apps/Settings/res/values/strings.xml"file. We add the unlock_set_biometric_strong below the "unlock_set_unlock_password_summary" as:

Biometric
Strong security, on trail
So that's the interface part, we need also figure out how the inner method call works.
Fig. 2 Face unlock settings sequence diagram
Fig. 2 illustrates the sequence diagram of how the interfaces interact. It all starts from the SecuritySettings class. The inheritance hierarchy is shown in Fig. 3.
Fig 3. SecuritySettings Class
We can see from the figure, eventually it extends Fragment class. According to android developers's page, "Fragment is a piece of an application's user interface or behavior that can be placed in an Activity. Interaction with fragments is done through FragmentManager, which can be obtained via Activity.getFragmentManager() and Fragment.getFragmentManager()". A Fragment is a small buildng block of the activity interface with its own life cycle depend on the activity life cycle. The method that conduct the UI inflation in this class is createPreferenceHierarchy. What this function does is generally load the UI componets and setup the preference. To load the UI fragment for screen security section, it retrieve the current screen lock password quality by invoking getKeyguardStoredPasswordQuality() method of LockPatternUtils class. For biometric authentication method, we add the usingBiometricStrong method to determine if face unlock method has been checked or not. If it is using face unlock, the resource id would be set to the face unlock preference screen interface id. Note that the function first check if the current unlock method is secure or not secure by the LockPatternUtils.isSecure function.
 public boolean isSecure() {
        long mode = getKeyguardStoredPasswordQuality();
        final boolean isPattern = mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
        final boolean isPassword = mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
                || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
                || mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
                || mode == DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
        final boolean isBiometricStrong = mode == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_STRONG;
        final boolean secure = isPattern && isLockPatternEnabled() && savedPatternExists()
                || isPassword && savedPasswordExists() || isBiometricStrong;
        return secure;
    }  
We add the biometric authentication also to be secure.
After that, it calls the addPreferencesFromResource of the PreferenceActivity class which "Inflates the given XML resource and adds the preference hierarchy to the current preference hierarchy".

Then as the other UI components, it waits for user action and pass control to other component. For PreferenceActivity class or subclasses, the action handler function in response to user click is onPreferenceTreeClick. It determines which preference element is being clicked by checking the preference key for each UI element. But what's the key of preference? Where was it set? Remember in the createPreferenceHierarchy we loaded the preference by resource id? It's right in the xml files that define the corresponding UI interfaces that the IDs are declared.
Following is the security_settings_biometric_strong.xml file that defines the settings interface when the biometric unlock is selected. We can see that line "android:key="unlock_set_or_change"" is where the preference key is set.
By checking the key, it starts the ChooseLockGeneric fragment when the screen lock entry is clicked.
final String key = preference.getKey();

        final LockPatternUtils lockPatternUtils = mChooseLockSettingsHelper.utils();
        if (KEY_UNLOCK_SET_OR_CHANGE.equals(key)) {
            startFragment(this, "com.android.settings.ChooseLockGeneric$ChooseLockGenericFragment",
                    SET_OR_CHANGE_LOCK_METHOD_REQUEST, null);
        } else if (KEY_BIOMETRIC_WEAK_IMPROVE_MATCHING.equals(key)) {
            ChooseLockSettingsHelper helper =
                    new ChooseLockSettingsHelper(this.getActivity(), this);
            if (!helper.launchConfirmationActivity(
                    CONFIRM_EXISTING_FOR_BIOMETRIC_WEAK_IMPROVE_REQUEST, null, null)) {
                // If this returns false, it means no password confirmation is required, so
                // go ahead and start improve. 
                // Note: currently a backup is required for biometric_weak so this code path
                // can't be reached, but is here in case things change in the future
                startBiometricWeakImprove();   
            }

And ChooseLockGenericFragment is also of the SettingsPreferenceFragment type. Take a look at the onCreate function of the fragment
 @Override             
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);                                                                                               

            mDPM = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);                                                     
            mKeyStore = KeyStore.getInstance();
            mChooseLockSettingsHelper = new ChooseLockSettingsHelper(this.getActivity());                                                     

            // Defaults to needing to confirm credentials
            final boolean confirmCredentials = getActivity().getIntent()
                .getBooleanExtra(CONFIRM_CREDENTIALS, true); 
            if (getActivity() instanceof ChooseLockGeneric.InternalActivity) {
                mPasswordConfirmed = !confirmCredentials;                                                                                     
            }

            if (savedInstanceState != null) {  
                mPasswordConfirmed = savedInstanceState.getBoolean(PASSWORD_CONFIRMED);
                mWaitingForConfirmation = savedInstanceState.getBoolean(WAITING_FOR_CONFIRMATION);
                mFinishPending = savedInstanceState.getBoolean(FINISH_PENDING);                                                               
            }

            if (mPasswordConfirmed) {          
                updatePreferencesOrFinish(); 
            } else if (!mWaitingForConfirmation) {
                ChooseLockSettingsHelper helper =      
                        new ChooseLockSettingsHelper(this.getActivity(), this);
                if (!helper.launchConfirmationActivity(CONFIRM_EXISTING_REQUEST, null, null)) {
                    mPasswordConfirmed = true; // no password set, so no need to confirm                                                      
                    updatePreferencesOrFinish(); 
                } else {      
                    mWaitingForConfirmation = true;                                                                                           
                }
            }
        }


the settings program doesn't show the unlock methods list to the user directly. It first checks if the current user has confirmed the unlock credential or not. If so, the undatePreferenceOrFinish function will be called.
 private void updatePreferencesOrFinish() {
            Intent intent = getActivity().getIntent();
            int quality = intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, -1);
            if (quality == -1) {
                // If caller didn't specify password quality, show UI and allow the user to choose.
                quality = intent.getIntExtra(MINIMUM_QUALITY_KEY, -1);
                MutableBoolean allowBiometric = new MutableBoolean(false);
                quality = upgradeQuality(quality, allowBiometric);
                final PreferenceScreen prefScreen = getPreferenceScreen();
                if (prefScreen != null) {
                    prefScreen.removeAll();
                }
                addPreferencesFromResource(R.xml.security_settings_picker);
                disableUnusablePreferences(quality, allowBiometric);
            } else {
                updateUnlockMethodAndFinish(quality, false);
            }
        }
In the caller doesn't already specify the password quality, it will load the security_settings_picker preference tree and let the user select unlock method. Otherwise, it goes directly to the updateUnlockMethodAndFinish function. If the control flow goes from the security_settings_picker and the user pick up the unlock method, it also goes to the updateUnlockMethodAndFinish. So let's check what happens in that method.
       void updateUnlockMethodAndFinish(int quality, boolean disabled) {
            // Sanity check. We should never get here without confirming user's existing password.
            if (!mPasswordConfirmed) {
                throw new IllegalStateException("Tried to update password without confirming it");
            }

            final boolean isFallback = getActivity().getIntent()
                .getBooleanExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false);

            quality = upgradeQuality(quality, null);

            if (quality >= DevicePolicyManager.PASSWORD_QUALITY_NUMERIC) {
                int minLength = mDPM.getPasswordMinimumLength(null);
                if (minLength < MIN_PASSWORD_LENGTH) {
                    minLength = MIN_PASSWORD_LENGTH;
                }
                final int maxLength = mDPM.getPasswordMaximumLength(quality);
                Intent intent = new Intent().setClass(getActivity(), ChooseLockPassword.class);
                intent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, quality);
                intent.putExtra(ChooseLockPassword.PASSWORD_MIN_KEY, minLength);
                intent.putExtra(ChooseLockPassword.PASSWORD_MAX_KEY, maxLength);
                intent.putExtra(CONFIRM_CREDENTIALS, false);
                intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK,
                        isFallback);
                if (isFallback) {
                    startActivityForResult(intent, FALLBACK_REQUEST);
                    return;
                } else {
                    mFinishPending = true;
                    intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
                    startActivity(intent);
                }
            } else if (quality == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) {
                Intent intent = new Intent(getActivity(), ChooseLockPattern.class);
            intent.putExtra("key_lock_method", "pattern");
                intent.putExtra(CONFIRM_CREDENTIALS, false);
                intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK,
                        isFallback);
                if (isFallback) {
                    startActivityForResult(intent, FALLBACK_REQUEST);
                    return;
                } else {
                    mFinishPending = true;
                    intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
                    startActivity(intent);
                }
            } else if (quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK) {
                Intent intent = getBiometricSensorIntent();
                mFinishPending = true;
                startActivity(intent);
            } else if (quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_STRONG) {
                Intent intent = new Intent();
                intent.setClass(getActivity(), ChooseLockBiometric.class);
                //intent.setClassName("edu.temple.dulab.biounlock", "edu.temple.dulab.biounlock.FaceEnroll");
                //intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
                mFinishPending = true;
                startActivity(intent);
            } else if (quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
                mChooseLockSettingsHelper.utils().clearLock(false);
                mChooseLockSettingsHelper.utils().setLockScreenDisabled(disabled);
                getActivity().setResult(Activity.RESULT_OK);
                finish();
            } else {
                finish();
    }
        }

Basically it switches control to other activity according to the selected password quality. So here we add the "PASSWORD_QUALITY_BIOMETRIC_STRONG" branch to direct the program to the face enrollment activity (ChooseLockBiometric). It that activity, we include a button to start face enrollment in the view.
       // FacerecActivity in com.philriesch.android.vflock;
                Intent intent = new Intent();
                intent.setClassName("com.philriesch.android.vflock", "com.philriesch.android.vflock.FacerecActivity");
                intent.putExtra(EXTRA_ENROLLMODE, true);
                startActivityForResult(intent, REQUEST_ENROLL_FACE);

We start the FacerecActivity located in the "com.philriesch.android.vflock" package. Once the activity result is received and is OK, we set the selected PASSWORD_TYPE_KEY to be PASSWORD_QUALITY_BIOMETRIC_STRONG and set the password quality for DPM in the LockPatternUtils instance.
That's the face enrollment extension to Android screen settings. Next we'll talk about extend keyguard to support face unlock.

1 comment:

  1. Very good post! We are linked to this great article on our website. Keep up the good writing. Find more information: How To Lock Files The Planet Using Just Your Blog.

    ReplyDelete