1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.policy; 18 19 import static android.hardware.biometrics.BiometricStateListener.STATE_BP_AUTH; 20 import static android.hardware.biometrics.BiometricStateListener.STATE_ENROLLING; 21 import static android.hardware.biometrics.BiometricStateListener.STATE_IDLE; 22 import static android.hardware.biometrics.BiometricStateListener.STATE_KEYGUARD_AUTH; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.content.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.pm.PackageManager; 31 import android.hardware.biometrics.BiometricStateListener; 32 import android.hardware.fingerprint.FingerprintManager; 33 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 34 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; 35 import android.os.Handler; 36 import android.os.PowerManager; 37 import android.util.Log; 38 import android.view.View; 39 import android.view.Window; 40 import android.view.WindowManager; 41 42 import com.android.internal.R; 43 import com.android.internal.annotations.VisibleForTesting; 44 45 import java.util.List; 46 import java.util.concurrent.atomic.AtomicBoolean; 47 48 /** 49 * Defines behavior for handling interactions between power button events and fingerprint-related 50 * operations, for devices where the fingerprint sensor (side fps) lives on the power button. 51 */ 52 public class SideFpsEventHandler implements View.OnClickListener { 53 54 private static final int DEBOUNCE_DELAY_MILLIS = 500; 55 56 private static final String TAG = "SideFpsEventHandler"; 57 58 @NonNull 59 private final Context mContext; 60 @NonNull 61 private final Handler mHandler; 62 @NonNull 63 private final PowerManager mPowerManager; 64 @NonNull 65 private final AtomicBoolean mSideFpsEventHandlerReady; 66 private final int mDismissDialogTimeout; 67 @Nullable 68 private SideFpsToast mDialog; 69 private final Runnable mTurnOffDialog = 70 () -> { 71 dismissDialog("mTurnOffDialog"); 72 }; 73 private @BiometricStateListener.State int mBiometricState; 74 private FingerprintManager mFingerprintManager; 75 private DialogProvider mDialogProvider; 76 private long mLastPowerPressTime; 77 SideFpsEventHandler( Context context, Handler handler, PowerManager powerManager)78 SideFpsEventHandler( 79 Context context, 80 Handler handler, 81 PowerManager powerManager) { 82 this(context, handler, powerManager, (ctx) -> { 83 SideFpsToast dialog = new SideFpsToast(ctx); 84 dialog.getWindow() 85 .setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); 86 dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 87 return dialog; 88 }); 89 } 90 91 @VisibleForTesting SideFpsEventHandler( Context context, Handler handler, PowerManager powerManager, DialogProvider provider)92 SideFpsEventHandler( 93 Context context, 94 Handler handler, 95 PowerManager powerManager, 96 DialogProvider provider) { 97 mContext = context; 98 mHandler = handler; 99 mPowerManager = powerManager; 100 mBiometricState = STATE_IDLE; 101 mSideFpsEventHandlerReady = new AtomicBoolean(false); 102 mDialogProvider = provider; 103 // ensure dialog is dismissed if screen goes off for unrelated reasons 104 context.registerReceiver( 105 new BroadcastReceiver() { 106 @Override 107 public void onReceive(Context context, Intent intent) { 108 if (mDialog != null) { 109 mDialog.dismiss(); 110 mDialog = null; 111 } 112 } 113 }, 114 new IntentFilter(Intent.ACTION_SCREEN_OFF)); 115 mDismissDialogTimeout = context.getResources().getInteger( 116 R.integer.config_sideFpsToastTimeout); 117 } 118 119 @Override onClick(View v)120 public void onClick(View v) { 121 goToSleep(mLastPowerPressTime); 122 } 123 124 /** 125 * Called from {@link PhoneWindowManager} to notify FingerprintManager that a single tap power 126 * button has been pressed. 127 */ notifyPowerPressed()128 public void notifyPowerPressed() { 129 Log.i(TAG, "notifyPowerPressed"); 130 if (mFingerprintManager == null && mSideFpsEventHandlerReady.get()) { 131 mFingerprintManager = mContext.getSystemService(FingerprintManager.class); 132 } 133 if (mFingerprintManager == null) { 134 return; 135 } 136 mFingerprintManager.onPowerPressed(); 137 } 138 139 /** 140 * Called from {@link PhoneWindowManager} and will dictate if the SideFpsEventHandler should 141 * handle the power press. 142 * 143 * @param eventTime powerPress event time 144 * @return true if powerPress was consumed, false otherwise 145 */ shouldConsumeSinglePress(long eventTime)146 public boolean shouldConsumeSinglePress(long eventTime) { 147 if (!mSideFpsEventHandlerReady.get()) { 148 return false; 149 } 150 151 switch (mBiometricState) { 152 case STATE_ENROLLING: 153 mHandler.post( 154 () -> { 155 if (mHandler.hasCallbacks(mTurnOffDialog)) { 156 Log.v(TAG, "Detected a tap to turn off dialog, ignoring"); 157 mHandler.removeCallbacks(mTurnOffDialog); 158 } 159 showDialog(eventTime, "Enroll Power Press"); 160 mHandler.postDelayed(mTurnOffDialog, mDismissDialogTimeout); 161 }); 162 return true; 163 case STATE_BP_AUTH: 164 return true; 165 case STATE_KEYGUARD_AUTH: 166 default: 167 return false; 168 } 169 } 170 goToSleep(long eventTime)171 private void goToSleep(long eventTime) { 172 mPowerManager.goToSleep( 173 eventTime, 174 PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 175 0 /* flags */); 176 } 177 178 /** 179 * Awaits notification from PhoneWindowManager that fingerprint service is ready to send updates 180 * about power button fps sensor state. Then configures a BiometricStateListener to receive and 181 * record updates to fps state, and registers the BiometricStateListener in FingerprintManager. 182 */ onFingerprintSensorReady()183 public void onFingerprintSensorReady() { 184 final PackageManager pm = mContext.getPackageManager(); 185 if (!pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { 186 return; 187 } 188 189 final FingerprintManager fingerprintManager = 190 mContext.getSystemService(FingerprintManager.class); 191 fingerprintManager.addAuthenticatorsRegisteredCallback( 192 new IFingerprintAuthenticatorsRegisteredCallback.Stub() { 193 @Override 194 public void onAllAuthenticatorsRegistered( 195 List<FingerprintSensorPropertiesInternal> sensors) { 196 if (fingerprintManager.isPowerbuttonFps()) { 197 fingerprintManager.registerBiometricStateListener( 198 new BiometricStateListener() { 199 @Nullable 200 private Runnable mStateRunnable = null; 201 202 @Override 203 public void onStateChanged( 204 @BiometricStateListener.State int newState) { 205 Log.d(TAG, "onStateChanged : " + newState); 206 if (mStateRunnable != null) { 207 mHandler.removeCallbacks(mStateRunnable); 208 mStateRunnable = null; 209 } 210 211 // When the user hits the power button the events can 212 // arrive in any order (success auth & power). Add a 213 // damper when moving to idle in case auth is first 214 if (newState == STATE_IDLE) { 215 mStateRunnable = () -> mBiometricState = newState; 216 // This is also useful in the case of biometric 217 // prompt. 218 // If a user has recently succeeded/failed auth, we 219 // want to disable the power button for a short 220 // period of time (so ethey are able to view the 221 // prompt) 222 mHandler.postDelayed( 223 mStateRunnable, DEBOUNCE_DELAY_MILLIS); 224 dismissDialog("STATE_IDLE"); 225 } else { 226 mBiometricState = newState; 227 } 228 } 229 230 @Override 231 public void onBiometricAction( 232 @BiometricStateListener.Action int action) { 233 Log.d(TAG, "onBiometricAction " + action); 234 } 235 }); 236 mSideFpsEventHandlerReady.set(true); 237 } 238 } 239 }); 240 } 241 dismissDialog(String reason)242 private void dismissDialog(String reason) { 243 Log.d(TAG, "Dismissing dialog with reason: " + reason); 244 if (mDialog != null && mDialog.isShowing()) { 245 mDialog.dismiss(); 246 } 247 } 248 showDialog(long time, String reason)249 private void showDialog(long time, String reason) { 250 Log.d(TAG, "Showing dialog with reason: " + reason); 251 if (mDialog != null && mDialog.isShowing()) { 252 Log.d(TAG, "Ignoring show dialog"); 253 return; 254 } 255 mDialog = mDialogProvider.provideDialog(mContext); 256 mLastPowerPressTime = time; 257 mDialog.show(); 258 mDialog.setOnClickListener(this); 259 } 260 261 interface DialogProvider { provideDialog(Context context)262 SideFpsToast provideDialog(Context context); 263 } 264 }