/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.policy.impl.keyguard; import com.android.internal.policy.IFaceLockCallback; import com.android.internal.policy.IFaceLockInterface; import com.android.internal.widget.LockPatternUtils; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.util.Log; import android.view.View; public class FaceUnlock implements BiometricSensorUnlock, Handler.Callback { private static final boolean DEBUG = false; private static final String TAG = "FULLockscreen"; private final Context mContext; private final LockPatternUtils mLockPatternUtils; // TODO: is mServiceRunning needed or can we just use mIsRunning or check if mService is null? private boolean mServiceRunning = false; // TODO: now that the code has been restructure to do almost all operations from a handler, this // lock may no longer be necessary. private final Object mServiceRunningLock = new Object(); private IFaceLockInterface mService; private boolean mBoundToService = false; private View mFaceUnlockView; private Handler mHandler; private final int MSG_SERVICE_CONNECTED = 0; private final int MSG_SERVICE_DISCONNECTED = 1; private final int MSG_UNLOCK = 2; private final int MSG_CANCEL = 3; private final int MSG_REPORT_FAILED_ATTEMPT = 4; private final int MSG_POKE_WAKELOCK = 5; // TODO: This was added for the purpose of adhering to what the biometric interface expects // the isRunning() function to return. However, it is probably not necessary to have both // mRunning and mServiceRunning. I'd just rather wait to change that logic. private volatile boolean mIsRunning = false; // So the user has a consistent amount of time when brought to the backup method from Face // Unlock private final int BACKUP_LOCK_TIMEOUT = 5000; KeyguardSecurityCallback mKeyguardScreenCallback; /** * Stores some of the structures that Face Unlock will need to access and creates the handler * will be used to execute messages on the UI thread. */ public FaceUnlock(Context context) { mContext = context; mLockPatternUtils = new LockPatternUtils(context); mHandler = new Handler(this); } public void setKeyguardCallback(KeyguardSecurityCallback keyguardScreenCallback) { mKeyguardScreenCallback = keyguardScreenCallback; } /** * Stores and displays the view that Face Unlock is allowed to draw within. * TODO: since the layout object will eventually be shared by multiple biometric unlock * methods, we will have to add our other views (background, cancel button) here. */ public void initializeView(View biometricUnlockView) { Log.d(TAG, "initializeView()"); mFaceUnlockView = biometricUnlockView; } /** * Indicates whether Face Unlock is currently running. */ public boolean isRunning() { return mIsRunning; } /** * Dismisses face unlock and goes to the backup lock */ public void stopAndShowBackup() { if (DEBUG) Log.d(TAG, "stopAndShowBackup()"); mHandler.sendEmptyMessage(MSG_CANCEL); } /** * Binds to the Face Unlock service. Face Unlock will be started when the bind completes. The * Face Unlock view is displayed to hide the backup lock while the service is starting up. * Called on the UI thread. */ public boolean start() { if (DEBUG) Log.d(TAG, "start()"); if (mHandler.getLooper() != Looper.myLooper()) { Log.e(TAG, "start() called off of the UI thread"); } if (mIsRunning) { Log.w(TAG, "start() called when already running"); } if (!mBoundToService) { Log.d(TAG, "Binding to Face Unlock service for user=" + mLockPatternUtils.getCurrentUser()); mContext.bindService(new Intent(IFaceLockInterface.class.getName()), mConnection, Context.BIND_AUTO_CREATE, mLockPatternUtils.getCurrentUser()); mBoundToService = true; } else { Log.w(TAG, "Attempt to bind to Face Unlock when already bound"); } mIsRunning = true; return true; } /** * Stops Face Unlock and unbinds from the service. Called on the UI thread. */ public boolean stop() { if (DEBUG) Log.d(TAG, "stop()"); if (mHandler.getLooper() != Looper.myLooper()) { Log.e(TAG, "stop() called from non-UI thread"); } // Clearing any old service connected messages. mHandler.removeMessages(MSG_SERVICE_CONNECTED); boolean mWasRunning = mIsRunning; stopUi(); if (mBoundToService) { if (mService != null) { try { mService.unregisterCallback(mFaceUnlockCallback); } catch (RemoteException e) { // Not much we can do } } Log.d(TAG, "Unbinding from Face Unlock service"); mContext.unbindService(mConnection); mBoundToService = false; } else { // This is usually not an error when this happens. Sometimes we will tell it to // unbind multiple times because it's called from both onWindowFocusChanged and // onDetachedFromWindow. if (DEBUG) Log.d(TAG, "Attempt to unbind from Face Unlock when not bound"); } mIsRunning = false; return mWasRunning; } /** * Frees up resources used by Face Unlock and stops it if it is still running. */ public void cleanUp() { if (DEBUG) Log.d(TAG, "cleanUp()"); if (mService != null) { try { mService.unregisterCallback(mFaceUnlockCallback); } catch (RemoteException e) { // Not much we can do } stopUi(); mService = null; } } /** * Returns the Device Policy Manager quality for Face Unlock, which is BIOMETRIC_WEAK. */ public int getQuality() { return DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK; } /** * Handles messages such that everything happens on the UI thread in a deterministic order. * Calls from the Face Unlock service come from binder threads. Calls from lockscreen typically * come from the UI thread. This makes sure there are no race conditions between those calls. */ public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_SERVICE_CONNECTED: handleServiceConnected(); break; case MSG_SERVICE_DISCONNECTED: handleServiceDisconnected(); break; case MSG_UNLOCK: handleUnlock(); break; case MSG_CANCEL: handleCancel(); break; case MSG_REPORT_FAILED_ATTEMPT: handleReportFailedAttempt(); break; case MSG_POKE_WAKELOCK: handlePokeWakelock(msg.arg1); break; default: Log.e(TAG, "Unhandled message"); return false; } return true; } /** * Tells the service to start its UI via an AIDL interface. Called when the * onServiceConnected() callback is received. */ void handleServiceConnected() { Log.d(TAG, "handleServiceConnected()"); // It is possible that an unbind has occurred in the time between the bind and when this // function is reached. If an unbind has already occurred, proceeding on to call startUi() // can result in a fatal error. Note that the onServiceConnected() callback is // asynchronous, so this possibility would still exist if we executed this directly in // onServiceConnected() rather than using a handler. if (!mBoundToService) { Log.d(TAG, "Dropping startUi() in handleServiceConnected() because no longer bound"); return; } try { mService.registerCallback(mFaceUnlockCallback); } catch (RemoteException e) { Log.e(TAG, "Caught exception connecting to Face Unlock: " + e.toString()); mService = null; mBoundToService = false; mIsRunning = false; return; } if (mFaceUnlockView != null) { IBinder windowToken = mFaceUnlockView.getWindowToken(); if (windowToken != null) { // When switching between portrait and landscape view while Face Unlock is running, // the screen will eventually go dark unless we poke the wakelock when Face Unlock // is restarted. mKeyguardScreenCallback.userActivity(0); int[] position; position = new int[2]; mFaceUnlockView.getLocationInWindow(position); startUi(windowToken, position[0], position[1], mFaceUnlockView.getWidth(), mFaceUnlockView.getHeight()); } else { Log.e(TAG, "windowToken is null in handleServiceConnected()"); } } } /** * Called when the onServiceDisconnected() callback is received. This should not happen during * normal operation. It indicates an error has occurred. */ void handleServiceDisconnected() { Log.e(TAG, "handleServiceDisconnected()"); // TODO: this lock may no longer be needed now that everything is being called from a // handler synchronized (mServiceRunningLock) { mService = null; mServiceRunning = false; } mBoundToService = false; mIsRunning = false; } /** * Stops the Face Unlock service and tells the device to grant access to the user. */ void handleUnlock() { if (DEBUG) Log.d(TAG, "handleUnlock()"); stop(); mKeyguardScreenCallback.reportSuccessfulUnlockAttempt(); mKeyguardScreenCallback.dismiss(true); } /** * Stops the Face Unlock service and goes to the backup lock. */ void handleCancel() { if (DEBUG) Log.d(TAG, "handleCancel()"); // We are going to the backup method, so we don't want to see Face Unlock again until the // next time the user visits keyguard. KeyguardUpdateMonitor.getInstance(mContext).setAlternateUnlockEnabled(false); mKeyguardScreenCallback.showBackupSecurity(); stop(); mKeyguardScreenCallback.userActivity(BACKUP_LOCK_TIMEOUT); } /** * Increments the number of failed Face Unlock attempts. */ void handleReportFailedAttempt() { if (DEBUG) Log.d(TAG, "handleReportFailedAttempt()"); // We are going to the backup method, so we don't want to see Face Unlock again until the // next time the user visits keyguard. KeyguardUpdateMonitor.getInstance(mContext).setAlternateUnlockEnabled(false); mKeyguardScreenCallback.reportFailedUnlockAttempt(); } /** * If the screen is on, pokes the wakelock to keep the screen alive and active for a specific * amount of time. */ void handlePokeWakelock(int millis) { PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); if (powerManager.isScreenOn()) { mKeyguardScreenCallback.userActivity(millis); } } /** * Implements service connection methods. */ private ServiceConnection mConnection = new ServiceConnection() { /** * Called when the Face Unlock service connects after calling bind(). */ public void onServiceConnected(ComponentName className, IBinder iservice) { Log.d(TAG, "Connected to Face Unlock service"); mService = IFaceLockInterface.Stub.asInterface(iservice); mHandler.sendEmptyMessage(MSG_SERVICE_CONNECTED); } /** * Called if the Face Unlock service unexpectedly disconnects. This indicates an error. */ public void onServiceDisconnected(ComponentName className) { Log.e(TAG, "Unexpected disconnect from Face Unlock service"); mHandler.sendEmptyMessage(MSG_SERVICE_DISCONNECTED); } }; /** * Tells the Face Unlock service to start displaying its UI and start processing. */ private void startUi(IBinder windowToken, int x, int y, int w, int h) { if (DEBUG) Log.d(TAG, "startUi()"); synchronized (mServiceRunningLock) { if (!mServiceRunning) { Log.d(TAG, "Starting Face Unlock"); try { mService.startUi(windowToken, x, y, w, h, mLockPatternUtils.isBiometricWeakLivelinessEnabled()); } catch (RemoteException e) { Log.e(TAG, "Caught exception starting Face Unlock: " + e.toString()); return; } mServiceRunning = true; } else { Log.w(TAG, "startUi() attempted while running"); } } } /** * Tells the Face Unlock service to stop displaying its UI and stop processing. */ private void stopUi() { if (DEBUG) Log.d(TAG, "stopUi()"); // Note that attempting to stop Face Unlock when it's not running is not an issue. // Face Unlock can return, which stops it and then we try to stop it when the // screen is turned off. That's why we check. synchronized (mServiceRunningLock) { if (mServiceRunning) { Log.d(TAG, "Stopping Face Unlock"); try { mService.stopUi(); } catch (RemoteException e) { Log.e(TAG, "Caught exception stopping Face Unlock: " + e.toString()); } mServiceRunning = false; } else { // This is usually not an error when this happens. Sometimes we will tell it to // stop multiple times because it's called from both onWindowFocusChanged and // onDetachedFromWindow. if (DEBUG) Log.d(TAG, "stopUi() attempted while not running"); } } } /** * Implements the AIDL biometric unlock service callback interface. */ private final IFaceLockCallback mFaceUnlockCallback = new IFaceLockCallback.Stub() { /** * Called when Face Unlock wants to grant access to the user. */ public void unlock() { if (DEBUG) Log.d(TAG, "unlock()"); mHandler.sendEmptyMessage(MSG_UNLOCK); } /** * Called when Face Unlock wants to go to the backup. */ public void cancel() { if (DEBUG) Log.d(TAG, "cancel()"); mHandler.sendEmptyMessage(MSG_CANCEL); } /** * Called when Face Unlock wants to increment the number of failed attempts. */ public void reportFailedAttempt() { if (DEBUG) Log.d(TAG, "reportFailedAttempt()"); mHandler.sendEmptyMessage(MSG_REPORT_FAILED_ATTEMPT); } /** * Called when Face Unlock wants to keep the screen alive and active for a specific amount * of time. */ public void pokeWakelock(int millis) { if (DEBUG) Log.d(TAG, "pokeWakelock() for " + millis + "ms"); Message message = mHandler.obtainMessage(MSG_POKE_WAKELOCK, millis, -1); mHandler.sendMessage(message); } }; }