Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2018 The Android Open Source Project |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 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 |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 14 | * limitations under the License |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 15 | */ |
| 16 | |
Kevin Chyn | 836f2cf | 2018-08-27 11:06:39 -0700 | [diff] [blame] | 17 | package com.android.server.biometrics; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 18 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 19 | import android.content.Context; |
Kevin Chyn | a56dff7 | 2018-06-19 18:41:12 -0700 | [diff] [blame] | 20 | import android.hardware.biometrics.BiometricAuthenticator; |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 21 | import android.hardware.biometrics.BiometricConstants; |
Beverly | d9ec6df | 2018-03-20 17:19:07 -0400 | [diff] [blame] | 22 | import android.media.AudioAttributes; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 23 | import android.os.IBinder; |
| 24 | import android.os.RemoteException; |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 25 | import android.os.VibrationEffect; |
| 26 | import android.os.Vibrator; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 27 | import android.util.Slog; |
| 28 | |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 29 | import com.android.internal.logging.MetricsLogger; |
| 30 | |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 31 | import java.util.ArrayList; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 32 | import java.util.NoSuchElementException; |
| 33 | |
| 34 | /** |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 35 | * Abstract base class for keeping track and dispatching events from the biometric's HAL to the |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 36 | * the current client. Subclasses are responsible for coordinating the interaction with |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 37 | * the biometric's HAL for the specific action (e.g. authenticate, enroll, enumerate, etc.). |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 38 | */ |
Kevin Chyn | 7782d14 | 2019-01-18 12:51:33 -0800 | [diff] [blame] | 39 | public abstract class ClientMonitor extends LoggableMonitor implements IBinder.DeathRecipient { |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 40 | protected static final int ERROR_ESRCH = 3; // Likely HAL is dead. See errno.h. |
Kevin Chyn | 355c6bf | 2018-09-20 22:14:19 -0700 | [diff] [blame] | 41 | protected static final boolean DEBUG = BiometricServiceBase.DEBUG; |
Beverly | d9ec6df | 2018-03-20 17:19:07 -0400 | [diff] [blame] | 42 | private static final AudioAttributes FINGERPRINT_SONFICATION_ATTRIBUTES = |
| 43 | new AudioAttributes.Builder() |
| 44 | .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) |
| 45 | .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) |
| 46 | .build(); |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 47 | |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 48 | private final Context mContext; |
| 49 | private final long mHalDeviceId; |
| 50 | private final int mTargetUserId; |
| 51 | private final int mGroupId; |
| 52 | // True if client does not have MANAGE_FINGERPRINT permission |
| 53 | private final boolean mIsRestricted; |
| 54 | private final String mOwner; |
| 55 | private final VibrationEffect mSuccessVibrationEffect; |
| 56 | private final VibrationEffect mErrorVibrationEffect; |
Kevin Chyn | 355c6bf | 2018-09-20 22:14:19 -0700 | [diff] [blame] | 57 | private final BiometricServiceBase.DaemonWrapper mDaemon; |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 58 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 59 | private IBinder mToken; |
Kevin Chyn | 355c6bf | 2018-09-20 22:14:19 -0700 | [diff] [blame] | 60 | private BiometricServiceBase.ServiceListener mListener; |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 61 | // Currently only used for authentication client. The cookie generated by BiometricService |
| 62 | // is never 0. |
| 63 | private final int mCookie; |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 64 | |
| 65 | protected final MetricsLogger mMetricsLogger; |
| 66 | protected final Metrics mMetrics; |
| 67 | |
Kevin Chyn | 3736858 | 2017-05-19 17:15:38 -0700 | [diff] [blame] | 68 | protected boolean mAlreadyCancelled; |
Kevin Chyn | c79856b | 2018-10-05 18:57:35 -0700 | [diff] [blame] | 69 | protected boolean mAlreadyDone; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 70 | |
| 71 | /** |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 72 | * @param context context of BiometricService |
| 73 | * @param daemon interface to call back to a specific biometric's daemon |
| 74 | * @param halDeviceId the HAL device ID of the associated biometric hardware |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 75 | * @param token a unique token for the client |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 76 | * @param listener recipient of related events (e.g. authentication) |
Jim Miller | 8f2aca0 | 2016-04-20 13:34:11 -0700 | [diff] [blame] | 77 | * @param userId target user id for operation |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 78 | * @param groupId groupId for the fingerprint set |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 79 | * @param restricted whether or not client has the MANAGE_* permission |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 80 | * permission |
| 81 | * @param owner name of the client that owns this |
| 82 | */ |
Kevin Chyn | 355c6bf | 2018-09-20 22:14:19 -0700 | [diff] [blame] | 83 | public ClientMonitor(Context context, Metrics metrics, |
| 84 | BiometricServiceBase.DaemonWrapper daemon, long halDeviceId, IBinder token, |
| 85 | BiometricServiceBase.ServiceListener listener, int userId, int groupId, |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 86 | boolean restricted, String owner, int cookie) { |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 87 | mContext = context; |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 88 | mMetrics = metrics; |
| 89 | mDaemon = daemon; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 90 | mHalDeviceId = halDeviceId; |
| 91 | mToken = token; |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 92 | mListener = listener; |
Jim Miller | 8f2aca0 | 2016-04-20 13:34:11 -0700 | [diff] [blame] | 93 | mTargetUserId = userId; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 94 | mGroupId = groupId; |
| 95 | mIsRestricted = restricted; |
| 96 | mOwner = owner; |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 97 | mCookie = cookie; |
Ilya Matyukhin | 21a0d1e | 2018-04-26 15:57:29 -0700 | [diff] [blame] | 98 | mSuccessVibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 99 | mErrorVibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 100 | mMetricsLogger = new MetricsLogger(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 101 | try { |
Jim Miller | 7e1cb55 | 2017-02-27 17:37:32 -0800 | [diff] [blame] | 102 | if (token != null) { |
| 103 | token.linkToDeath(this, 0); |
| 104 | } |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 105 | } catch (RemoteException e) { |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 106 | Slog.w(getLogTag(), "caught remote exception in linkToDeath: ", e); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 107 | } |
| 108 | } |
| 109 | |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 110 | protected String getLogTag() { |
| 111 | return mMetrics.logTag(); |
| 112 | } |
| 113 | |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 114 | public int getCookie() { |
| 115 | return mCookie; |
| 116 | } |
| 117 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 118 | /** |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 119 | * Contacts the biometric's HAL to start the client. |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 120 | * @return 0 on success, errno from driver on failure |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 121 | */ |
| 122 | public abstract int start(); |
| 123 | |
| 124 | /** |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 125 | * Contacts the biometric's HAL to stop the client. |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 126 | * @param initiatedByClient whether the operation is at the request of a client |
| 127 | */ |
| 128 | public abstract int stop(boolean initiatedByClient); |
| 129 | |
| 130 | /** |
| 131 | * Method to explicitly poke powermanager on events |
| 132 | */ |
| 133 | public abstract void notifyUserActivity(); |
| 134 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 135 | // Event callbacks from driver. Inappropriate calls is flagged/logged by the |
| 136 | // respective client (e.g. enrolling shouldn't get authenticate events). |
| 137 | // All of these return 'true' if the operation is completed and it's ok to move |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 138 | // to the next client (e.g. authentication accepts or rejects a biometric). |
Kevin Chyn | a56dff7 | 2018-06-19 18:41:12 -0700 | [diff] [blame] | 139 | public abstract boolean onEnrollResult(BiometricAuthenticator.Identifier identifier, |
| 140 | int remaining); |
Kevin Chyn | b528d69 | 2018-07-20 11:53:14 -0700 | [diff] [blame] | 141 | public abstract boolean onAuthenticated(BiometricAuthenticator.Identifier identifier, |
Kevin Chyn | 6cf54e8 | 2018-09-18 19:13:27 -0700 | [diff] [blame] | 142 | boolean authenticated, ArrayList<Byte> token); |
Kevin Chyn | a56dff7 | 2018-06-19 18:41:12 -0700 | [diff] [blame] | 143 | public abstract boolean onRemoved(BiometricAuthenticator.Identifier identifier, |
| 144 | int remaining); |
| 145 | public abstract boolean onEnumerationResult( |
| 146 | BiometricAuthenticator.Identifier identifier, int remaining); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 147 | |
Kevin Chyn | c79856b | 2018-10-05 18:57:35 -0700 | [diff] [blame] | 148 | |
| 149 | public boolean isAlreadyDone() { |
| 150 | return mAlreadyDone; |
| 151 | } |
| 152 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 153 | /** |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 154 | * Called when we get notification from the biometric's HAL that an image has been acquired. |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 155 | * Common to authenticate and enroll. |
| 156 | * @param acquiredInfo info about the current image acquisition |
| 157 | * @return true if client should be removed |
| 158 | */ |
Jim Miller | 40e4645 | 2016-12-16 18:38:53 -0800 | [diff] [blame] | 159 | public boolean onAcquired(int acquiredInfo, int vendorCode) { |
Kevin Chyn | 7782d14 | 2019-01-18 12:51:33 -0800 | [diff] [blame] | 160 | super.logOnAcquired(acquiredInfo, vendorCode, getTargetUserId()); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 161 | try { |
Kevin Chyn | a56dff7 | 2018-06-19 18:41:12 -0700 | [diff] [blame] | 162 | if (mListener != null) { |
| 163 | mListener.onAcquired(getHalDeviceId(), acquiredInfo, vendorCode); |
| 164 | } |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 165 | return false; // acquisition continues... |
| 166 | } catch (RemoteException e) { |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 167 | Slog.w(getLogTag(), "Failed to invoke sendAcquired", e); |
| 168 | return true; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 169 | } finally { |
| 170 | // Good scans will keep the device awake |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 171 | if (acquiredInfo == BiometricConstants.BIOMETRIC_ACQUIRED_GOOD) { |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 172 | notifyUserActivity(); |
| 173 | } |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | /** |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 178 | * Called when we get notification from the biometric's HAL that an error has occurred with the |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 179 | * current operation. Common to authenticate, enroll, enumerate and remove. |
| 180 | * @param error |
| 181 | * @return true if client should be removed |
| 182 | */ |
Kevin Chyn | a56dff7 | 2018-06-19 18:41:12 -0700 | [diff] [blame] | 183 | public boolean onError(long deviceId, int error, int vendorCode) { |
Kevin Chyn | 7782d14 | 2019-01-18 12:51:33 -0800 | [diff] [blame] | 184 | super.logOnError(error, vendorCode, getTargetUserId()); |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 185 | try { |
Kevin Chyn | a56dff7 | 2018-06-19 18:41:12 -0700 | [diff] [blame] | 186 | if (mListener != null) { |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 187 | mListener.onError(deviceId, error, vendorCode, getCookie()); |
Kevin Chyn | a56dff7 | 2018-06-19 18:41:12 -0700 | [diff] [blame] | 188 | } |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 189 | } catch (RemoteException e) { |
| 190 | Slog.w(getLogTag(), "Failed to invoke sendError", e); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 191 | } |
| 192 | return true; // errors always remove current client |
| 193 | } |
| 194 | |
| 195 | public void destroy() { |
| 196 | if (mToken != null) { |
| 197 | try { |
| 198 | mToken.unlinkToDeath(this, 0); |
| 199 | } catch (NoSuchElementException e) { |
| 200 | // TODO: remove when duplicate call bug is found |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 201 | Slog.e(getLogTag(), "destroy(): " + this + ":", new Exception("here")); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 202 | } |
| 203 | mToken = null; |
| 204 | } |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 205 | mListener = null; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 206 | } |
| 207 | |
| 208 | @Override |
| 209 | public void binderDied() { |
Kevin Chyn | 9e7b89a | 2018-11-20 11:24:49 -0800 | [diff] [blame] | 210 | // If the current client dies we should cancel the current operation. |
| 211 | Slog.e(getLogTag(), "Binder died, cancelling client"); |
Kevin Chyn | 1417f1f | 2019-02-25 11:38:57 -0800 | [diff] [blame] | 212 | stop(false /* initiatedByClient */); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 213 | mToken = null; |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 214 | mListener = null; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 215 | } |
| 216 | |
| 217 | @Override |
| 218 | protected void finalize() throws Throwable { |
| 219 | try { |
| 220 | if (mToken != null) { |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 221 | if (DEBUG) Slog.w(getLogTag(), "removing leaked reference: " + mToken); |
Kevin Chyn | a56dff7 | 2018-06-19 18:41:12 -0700 | [diff] [blame] | 222 | onError(getHalDeviceId(), BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE, |
| 223 | 0 /* vendorCode */); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 224 | } |
| 225 | } finally { |
| 226 | super.finalize(); |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | public final Context getContext() { |
| 231 | return mContext; |
| 232 | } |
| 233 | |
| 234 | public final long getHalDeviceId() { |
| 235 | return mHalDeviceId; |
| 236 | } |
| 237 | |
| 238 | public final String getOwnerString() { |
| 239 | return mOwner; |
| 240 | } |
| 241 | |
Kevin Chyn | 355c6bf | 2018-09-20 22:14:19 -0700 | [diff] [blame] | 242 | public final BiometricServiceBase.ServiceListener getListener() { |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 243 | return mListener; |
| 244 | } |
| 245 | |
Kevin Chyn | 355c6bf | 2018-09-20 22:14:19 -0700 | [diff] [blame] | 246 | public final BiometricServiceBase.DaemonWrapper getDaemonWrapper() { |
Kevin Chyn | 037c4d5 | 2018-06-11 19:17:32 -0700 | [diff] [blame] | 247 | return mDaemon; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 248 | } |
| 249 | |
| 250 | public final boolean getIsRestricted() { |
| 251 | return mIsRestricted; |
| 252 | } |
| 253 | |
Jim Miller | 8f2aca0 | 2016-04-20 13:34:11 -0700 | [diff] [blame] | 254 | public final int getTargetUserId() { |
| 255 | return mTargetUserId; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 256 | } |
| 257 | |
| 258 | public final int getGroupId() { |
| 259 | return mGroupId; |
| 260 | } |
| 261 | |
| 262 | public final IBinder getToken() { |
| 263 | return mToken; |
| 264 | } |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 265 | |
| 266 | public final void vibrateSuccess() { |
| 267 | Vibrator vibrator = mContext.getSystemService(Vibrator.class); |
| 268 | if (vibrator != null) { |
Beverly | d9ec6df | 2018-03-20 17:19:07 -0400 | [diff] [blame] | 269 | vibrator.vibrate(mSuccessVibrationEffect, FINGERPRINT_SONFICATION_ATTRIBUTES); |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 270 | } |
| 271 | } |
| 272 | |
| 273 | public final void vibrateError() { |
| 274 | Vibrator vibrator = mContext.getSystemService(Vibrator.class); |
| 275 | if (vibrator != null) { |
Beverly | d9ec6df | 2018-03-20 17:19:07 -0400 | [diff] [blame] | 276 | vibrator.vibrate(mErrorVibrationEffect, FINGERPRINT_SONFICATION_ATTRIBUTES); |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 277 | } |
| 278 | } |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 279 | } |