Merge "Fix bug where fingerprint events can be delivered to the wrong client" into nyc-dev
This commit is contained in:
@@ -517,7 +517,8 @@ public class FingerprintManager {
|
||||
|
||||
if (mService != null) try {
|
||||
mEnrollmentCallback = callback;
|
||||
mService.enroll(mToken, token, userId, mServiceReceiver, flags);
|
||||
mService.enroll(mToken, token, userId, mServiceReceiver, flags,
|
||||
mContext.getOpPackageName());
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Remote exception in enroll: ", e);
|
||||
if (callback != null) {
|
||||
|
||||
@@ -35,4 +35,6 @@ interface IFingerprintDaemon {
|
||||
int closeHal();
|
||||
void init(IFingerprintDaemonCallback callback);
|
||||
int postEnroll();
|
||||
int enumerate();
|
||||
int cancelEnumeration();
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ interface IFingerprintService {
|
||||
|
||||
// Start fingerprint enrollment
|
||||
void enroll(IBinder token, in byte [] cryptoToken, int groupId, IFingerprintServiceReceiver receiver,
|
||||
int flags);
|
||||
int flags, String opPackageName);
|
||||
|
||||
// Cancel enrollment in progress
|
||||
void cancelEnrollment(IBinder token);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright (C) 2016 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.server.fingerprint;
|
||||
|
||||
import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.internal.logging.MetricsProto.MetricsEvent;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.Fingerprint;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.hardware.fingerprint.IFingerprintDaemon;
|
||||
import android.hardware.fingerprint.IFingerprintServiceReceiver;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.system.ErrnoException;
|
||||
import android.util.Slog;
|
||||
|
||||
/**
|
||||
* A class to keep track of the authentication state for a given client.
|
||||
*/
|
||||
public abstract class AuthenticationClient extends ClientMonitor {
|
||||
private long mOpId;
|
||||
|
||||
public abstract boolean handleFailedAttempt();
|
||||
public abstract void resetFailedAttempts();
|
||||
|
||||
public AuthenticationClient(Context context, long halDeviceId, IBinder token,
|
||||
IFingerprintServiceReceiver receiver, int userId, int groupId, long opId,
|
||||
boolean restricted, String owner) {
|
||||
super(context, halDeviceId, token, receiver, userId, groupId, restricted, owner);
|
||||
mOpId = opId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAuthenticated(int fingerId, int groupId) {
|
||||
boolean result = false;
|
||||
boolean authenticated = fingerId != 0;
|
||||
|
||||
IFingerprintServiceReceiver receiver = getReceiver();
|
||||
if (receiver != null) {
|
||||
try {
|
||||
MetricsLogger.action(getContext(), MetricsEvent.ACTION_FINGERPRINT_AUTH,
|
||||
authenticated);
|
||||
if (!authenticated) {
|
||||
receiver.onAuthenticationFailed(getHalDeviceId());
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Slog.v(TAG, "onAuthenticated(owner=" + getOwnerString()
|
||||
+ ", id=" + fingerId + ", gp=" + groupId + ")");
|
||||
}
|
||||
Fingerprint fp = !getIsRestricted()
|
||||
? new Fingerprint("" /* TODO */, groupId, fingerId, getHalDeviceId())
|
||||
: null;
|
||||
receiver.onAuthenticationSucceeded(getHalDeviceId(), fp);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to notify Authenticated:", e);
|
||||
result = true; // client failed
|
||||
}
|
||||
} else {
|
||||
result = true; // client not listening
|
||||
}
|
||||
if (fingerId == 0) {
|
||||
if (receiver != null) {
|
||||
FingerprintUtils.vibrateFingerprintError(getContext());
|
||||
}
|
||||
// allow system-defined limit of number of attempts before giving up
|
||||
result |= handleFailedAttempt();
|
||||
} else {
|
||||
if (receiver != null) {
|
||||
FingerprintUtils.vibrateFingerprintSuccess(getContext());
|
||||
}
|
||||
result |= true; // we have a valid fingerprint, done
|
||||
resetFailedAttempts();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start authentication
|
||||
*/
|
||||
@Override
|
||||
public int start() {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "start authentication: no fingeprintd!");
|
||||
return ERROR_ESRCH;
|
||||
}
|
||||
try {
|
||||
final int result = daemon.authenticate(mOpId, getGroupId());
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "startAuthentication failed, result=" + result);
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
return result;
|
||||
}
|
||||
if (DEBUG) Slog.w(TAG, "client " + getOwnerString() + " is authenticating...");
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "startAuthentication failed", e);
|
||||
return ERROR_ESRCH;
|
||||
}
|
||||
return 0; // success
|
||||
}
|
||||
|
||||
@Override
|
||||
public int stop(boolean initiatedByClient) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "stopAuthentication: no fingeprintd!");
|
||||
return ERROR_ESRCH;
|
||||
}
|
||||
try {
|
||||
final int result = daemon.cancelAuthentication();
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "stopAuthentication failed, result=" + result);
|
||||
return result;
|
||||
}
|
||||
if (DEBUG) Slog.w(TAG, "client " + getOwnerString() + " is no longer authenticating");
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "stopAuthentication failed", e);
|
||||
return ERROR_ESRCH;
|
||||
}
|
||||
return 0; // success
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEnrollResult(int fingerId, int groupId, int rem) {
|
||||
if (DEBUG) Slog.w(TAG, "onEnrollResult() called for authenticate!");
|
||||
return true; // Invalid for Authenticate
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRemoved(int fingerId, int groupId) {
|
||||
if (DEBUG) Slog.w(TAG, "onRemoved() called for authenticate!");
|
||||
return true; // Invalid for Authenticate
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEnumerationResult(int fingerId, int groupId) {
|
||||
if (DEBUG) Slog.w(TAG, "onEnumerationResult() called for authenticate!");
|
||||
return true; // Invalid for Authenticate
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright (C) 2016 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.server.fingerprint;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.hardware.fingerprint.IFingerprintDaemon;
|
||||
import android.hardware.fingerprint.IFingerprintServiceReceiver;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Slog;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Abstract base class for keeping track and dispatching events from fingerprintd to the
|
||||
* the current client. Subclasses are responsible for coordinating the interaction with
|
||||
* fingerprintd for the specific action (e.g. authenticate, enroll, enumerate, etc.).
|
||||
*/
|
||||
public abstract class ClientMonitor implements IBinder.DeathRecipient {
|
||||
protected static final String TAG = FingerprintService.TAG; // TODO: get specific name
|
||||
protected static final int ERROR_ESRCH = 3; // Likely fingerprintd is dead. See errno.h.
|
||||
protected static final boolean DEBUG = FingerprintService.DEBUG;
|
||||
private IBinder mToken;
|
||||
private IFingerprintServiceReceiver mReceiver;
|
||||
private int mUserId;
|
||||
private int mGroupId;
|
||||
private boolean mIsRestricted; // True if client does not have MANAGE_FINGERPRINT permission
|
||||
private String mOwner;
|
||||
private Context mContext;
|
||||
private long mHalDeviceId;
|
||||
|
||||
/**
|
||||
* @param context context of FingerprintService
|
||||
* @param halDeviceId the HAL device ID of the associated fingerprint hardware
|
||||
* @param token a unique token for the client
|
||||
* @param receiver recipient of related events (e.g. authentication)
|
||||
* @param userId userId for the fingerprint set
|
||||
* @param groupId groupId for the fingerprint set
|
||||
* @param restricted whether or not client has the {@link Manifest#MANAGE_FINGERPRINT}
|
||||
* permission
|
||||
* @param owner name of the client that owns this
|
||||
*/
|
||||
public ClientMonitor(Context context, long halDeviceId, IBinder token,
|
||||
IFingerprintServiceReceiver receiver, int userId, int groupId,boolean restricted,
|
||||
String owner) {
|
||||
mContext = context;
|
||||
mHalDeviceId = halDeviceId;
|
||||
mToken = token;
|
||||
mReceiver = receiver;
|
||||
mUserId = userId;
|
||||
mGroupId = groupId;
|
||||
mIsRestricted = restricted;
|
||||
mOwner = owner;
|
||||
try {
|
||||
token.linkToDeath(this, 0);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "caught remote exception in linkToDeath: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contacts fingerprintd to start the client.
|
||||
* @return 0 on succes, errno from driver on failure
|
||||
*/
|
||||
public abstract int start();
|
||||
|
||||
/**
|
||||
* Contacts fingerprintd to stop the client.
|
||||
* @param initiatedByClient whether the operation is at the request of a client
|
||||
*/
|
||||
public abstract int stop(boolean initiatedByClient);
|
||||
|
||||
/**
|
||||
* Method to explicitly poke powermanager on events
|
||||
*/
|
||||
public abstract void notifyUserActivity();
|
||||
|
||||
/**
|
||||
* Gets the fingerprint daemon from the cached state in the container class.
|
||||
*/
|
||||
public abstract IFingerprintDaemon getFingerprintDaemon();
|
||||
|
||||
// Event callbacks from driver. Inappropriate calls is flagged/logged by the
|
||||
// respective client (e.g. enrolling shouldn't get authenticate events).
|
||||
// All of these return 'true' if the operation is completed and it's ok to move
|
||||
// to the next client (e.g. authentication accepts or rejects a fingerprint).
|
||||
public abstract boolean onEnrollResult(int fingerId, int groupId, int rem);
|
||||
public abstract boolean onAuthenticated(int fingerId, int groupId);
|
||||
public abstract boolean onRemoved(int fingerId, int groupId);
|
||||
public abstract boolean onEnumerationResult(int fingerId, int groupId);
|
||||
|
||||
/**
|
||||
* Called when we get notification from fingerprintd that an image has been acquired.
|
||||
* Common to authenticate and enroll.
|
||||
* @param acquiredInfo info about the current image acquisition
|
||||
* @return true if client should be removed
|
||||
*/
|
||||
public boolean onAcquired(int acquiredInfo) {
|
||||
if (mReceiver == null)
|
||||
return true; // client not connected
|
||||
try {
|
||||
mReceiver.onAcquired(getHalDeviceId(), acquiredInfo);
|
||||
return false; // acquisition continues...
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to invoke sendAcquired:", e);
|
||||
return true; // client failed
|
||||
} finally {
|
||||
// Good scans will keep the device awake
|
||||
if (acquiredInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD) {
|
||||
notifyUserActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we get notification from fingerprintd that an error has occurred with the
|
||||
* current operation. Common to authenticate, enroll, enumerate and remove.
|
||||
* @param error
|
||||
* @return true if client should be removed
|
||||
*/
|
||||
public boolean onError(int error) {
|
||||
if (mReceiver != null) {
|
||||
try {
|
||||
mReceiver.onError(getHalDeviceId(), error);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to invoke sendError:", e);
|
||||
}
|
||||
}
|
||||
return true; // errors always remove current client
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (mToken != null) {
|
||||
try {
|
||||
mToken.unlinkToDeath(this, 0);
|
||||
} catch (NoSuchElementException e) {
|
||||
// TODO: remove when duplicate call bug is found
|
||||
Slog.e(TAG, "destroy(): " + this + ":", new Exception("here"));
|
||||
}
|
||||
mToken = null;
|
||||
}
|
||||
mReceiver = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
mToken = null;
|
||||
mReceiver = null;
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
try {
|
||||
if (mToken != null) {
|
||||
if (DEBUG) Slog.w(TAG, "removing leaked reference: " + mToken);
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
}
|
||||
} finally {
|
||||
super.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
public final Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
public final long getHalDeviceId() {
|
||||
return mHalDeviceId;
|
||||
}
|
||||
|
||||
public final String getOwnerString() {
|
||||
return mOwner;
|
||||
}
|
||||
|
||||
public final IFingerprintServiceReceiver getReceiver() {
|
||||
return mReceiver;
|
||||
}
|
||||
|
||||
public final boolean getIsRestricted() {
|
||||
return mIsRestricted;
|
||||
}
|
||||
|
||||
public final int getUserId() {
|
||||
return mUserId;
|
||||
}
|
||||
|
||||
public final int getGroupId() {
|
||||
return mGroupId;
|
||||
}
|
||||
|
||||
public final IBinder getToken() {
|
||||
return mToken;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (C) 2016 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.server.fingerprint;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.hardware.fingerprint.IFingerprintDaemon;
|
||||
import android.hardware.fingerprint.IFingerprintServiceReceiver;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.internal.logging.MetricsProto.MetricsEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A class to keep track of the enrollment state for a given client.
|
||||
*/
|
||||
public abstract class EnrollClient extends ClientMonitor {
|
||||
private static final long MS_PER_SEC = 1000;
|
||||
private static final int ENROLLMENT_TIMEOUT_MS = 60 * 1000; // 1 minute
|
||||
private byte[] mCryptoToken;
|
||||
|
||||
public EnrollClient(Context context, long halDeviceId, IBinder token,
|
||||
IFingerprintServiceReceiver receiver, int userId, int groupId, byte [] cryptoToken,
|
||||
boolean restricted, String owner) {
|
||||
super(context, halDeviceId, token, receiver, userId, groupId, restricted, owner);
|
||||
mCryptoToken = Arrays.copyOf(cryptoToken, cryptoToken.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEnrollResult(int fingerId, int groupId, int remaining) {
|
||||
if (remaining == 0) {
|
||||
FingerprintUtils.getInstance().addFingerprintForUser(getContext(), fingerId,
|
||||
getUserId());
|
||||
}
|
||||
return sendEnrollResult(fingerId, groupId, remaining);
|
||||
}
|
||||
|
||||
/*
|
||||
* @return true if we're done.
|
||||
*/
|
||||
private boolean sendEnrollResult(int fpId, int groupId, int remaining) {
|
||||
IFingerprintServiceReceiver receiver = getReceiver();
|
||||
if (receiver == null)
|
||||
return true; // client not listening
|
||||
|
||||
FingerprintUtils.vibrateFingerprintSuccess(getContext());
|
||||
MetricsLogger.action(getContext(), MetricsEvent.ACTION_FINGERPRINT_ENROLL);
|
||||
try {
|
||||
receiver.onEnrollResult(getHalDeviceId(), fpId, groupId, remaining);
|
||||
return remaining == 0;
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to notify EnrollResult:", e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int start() {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "enroll: no fingeprintd!");
|
||||
return ERROR_ESRCH;
|
||||
}
|
||||
final int timeout = (int) (ENROLLMENT_TIMEOUT_MS / MS_PER_SEC);
|
||||
try {
|
||||
final int result = daemon.enroll(mCryptoToken, getGroupId(), timeout);
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "startEnroll failed, result=" + result);
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
return result;
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "startEnroll failed", e);
|
||||
}
|
||||
return 0; // success
|
||||
}
|
||||
|
||||
@Override
|
||||
public int stop(boolean initiatedByClient) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "stopEnrollment: no fingeprintd!");
|
||||
return ERROR_ESRCH;
|
||||
}
|
||||
try {
|
||||
final int result = daemon.cancelEnrollment();
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "startEnrollCancel failed, result = " + result);
|
||||
return result;
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "stopEnrollment failed", e);
|
||||
}
|
||||
if (initiatedByClient) {
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_CANCELED);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRemoved(int fingerId, int groupId) {
|
||||
if (DEBUG) Slog.w(TAG, "onRemoved() called for enroll!");
|
||||
return true; // Invalid for EnrollClient
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEnumerationResult(int fingerId, int groupId) {
|
||||
if (DEBUG) Slog.w(TAG, "onEnumerationResult() called for enroll!");
|
||||
return true; // Invalid for EnrollClient
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAuthenticated(int fingerId, int groupId) {
|
||||
if (DEBUG) Slog.w(TAG, "onAuthenticated() called for enroll!");
|
||||
return true; // Invalid for EnrollClient
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (C) 2016 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.server.fingerprint;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.hardware.fingerprint.IFingerprintDaemon;
|
||||
import android.hardware.fingerprint.IFingerprintServiceReceiver;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Slog;
|
||||
|
||||
/**
|
||||
* A class to keep track of the enumeration state for a given client.
|
||||
*/
|
||||
public abstract class EnumerateClient extends ClientMonitor {
|
||||
public EnumerateClient(Context context, long halDeviceId, IBinder token,
|
||||
IFingerprintServiceReceiver receiver, int userId, int groupId,
|
||||
boolean restricted, String owner) {
|
||||
super(context, halDeviceId, token, receiver, userId, groupId, restricted, owner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int start() {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
// The fingerprint template ids will be removed when we get confirmation from the HAL
|
||||
try {
|
||||
final int result = daemon.enumerate();
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "start enumerate for user " + getUserId()
|
||||
+ " failed, result=" + result);
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
return result;
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "startRemove failed", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int stop(boolean initiatedByClient) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "stopAuthentication: no fingeprintd!");
|
||||
return ERROR_ESRCH;
|
||||
}
|
||||
try {
|
||||
final int result = daemon.cancelEnumeration();
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "stop enumeration failed, result=" + result);
|
||||
return result;
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "stop enumeration failed", e);
|
||||
return ERROR_ESRCH;
|
||||
}
|
||||
// We don't actually stop enumerate, but inform the client that the cancel operation
|
||||
// succeeded so we can start the next operation.
|
||||
if (initiatedByClient) {
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_CANCELED);
|
||||
}
|
||||
return 0; // success
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEnumerationResult(int fingerId, int groupId) {
|
||||
IFingerprintServiceReceiver receiver = getReceiver();
|
||||
if (receiver == null)
|
||||
return true; // client not listening
|
||||
try {
|
||||
receiver.onRemoved(getHalDeviceId(), fingerId, groupId);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to notify enumerated:", e);
|
||||
}
|
||||
return fingerId == 0; // done when id hits 0
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ package com.android.server.fingerprint;
|
||||
import android.Manifest;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManager.RunningAppProcessInfo;
|
||||
import android.app.trust.TrustManager;
|
||||
import android.app.ActivityManagerNative;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.AppOpsManager;
|
||||
@@ -48,7 +47,6 @@ import android.os.UserManager;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.internal.logging.MetricsProto.MetricsEvent;
|
||||
import com.android.server.SystemService;
|
||||
|
||||
import org.json.JSONArray;
|
||||
@@ -75,7 +73,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* A service to manage multiple clients that want to access the fingerprint HAL API.
|
||||
@@ -85,31 +82,36 @@ import java.util.NoSuchElementException;
|
||||
* @hide
|
||||
*/
|
||||
public class FingerprintService extends SystemService implements IBinder.DeathRecipient {
|
||||
private static final String TAG = "FingerprintService";
|
||||
private static final boolean DEBUG = true;
|
||||
static final String TAG = "FingerprintService";
|
||||
static final boolean DEBUG = true;
|
||||
private static final String FP_DATA_DIR = "fpdata";
|
||||
private static final String FINGERPRINTD = "android.hardware.fingerprint.IFingerprintDaemon";
|
||||
private static final int MSG_USER_SWITCHING = 10;
|
||||
private static final int ENROLLMENT_TIMEOUT_MS = 60 * 1000; // 1 minute
|
||||
private static final String ACTION_LOCKOUT_RESET =
|
||||
"com.android.server.fingerprint.ACTION_LOCKOUT_RESET";
|
||||
|
||||
private ClientMonitor mAuthClient = null;
|
||||
private ClientMonitor mEnrollClient = null;
|
||||
private ClientMonitor mRemoveClient = null;
|
||||
private final ArrayList<FingerprintServiceLockoutResetMonitor> mLockoutMonitors =
|
||||
new ArrayList<>();
|
||||
private final AppOpsManager mAppOps;
|
||||
|
||||
private static final long MS_PER_SEC = 1000;
|
||||
private static final long FAIL_LOCKOUT_TIMEOUT_MS = 30*1000;
|
||||
private static final int MAX_FAILED_ATTEMPTS = 5;
|
||||
private static final int FINGERPRINT_ACQUIRED_GOOD = 0;
|
||||
private static final long CANCEL_TIMEOUT_LIMIT = 3000; // max wait for onCancel() from HAL,in ms
|
||||
private final String mKeyguardPackage;
|
||||
private int mCurrentUserId = UserHandle.USER_CURRENT;
|
||||
private int mUserIdForRemove = UserHandle.USER_NULL;
|
||||
private final FingerprintUtils mFingerprintUtils = FingerprintUtils.getInstance();
|
||||
private Context mContext;
|
||||
private long mHalDeviceId;
|
||||
private int mFailedAttempts;
|
||||
private IFingerprintDaemon mDaemon;
|
||||
private final PowerManager mPowerManager;
|
||||
private final AlarmManager mAlarmManager;
|
||||
private final UserManager mUserManager;
|
||||
private ClientMonitor mCurrentClient;
|
||||
private ClientMonitor mPendingClient;
|
||||
private long mCurrentAuthenticatorId;
|
||||
|
||||
Handler mHandler = new Handler() {
|
||||
private Handler mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(android.os.Message msg) {
|
||||
switch (msg.what) {
|
||||
@@ -123,15 +125,6 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
}
|
||||
};
|
||||
|
||||
private final FingerprintUtils mFingerprintUtils = FingerprintUtils.getInstance();
|
||||
private Context mContext;
|
||||
private long mHalDeviceId;
|
||||
private int mFailedAttempts;
|
||||
private IFingerprintDaemon mDaemon;
|
||||
private final PowerManager mPowerManager;
|
||||
private final AlarmManager mAlarmManager;
|
||||
private final UserManager mUserManager;
|
||||
|
||||
private final BroadcastReceiver mLockoutReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@@ -148,6 +141,26 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable mResetClientState = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Warning: if we get here, the driver never confirmed our call to cancel the current
|
||||
// operation (authenticate, enroll, remove, enumerate, etc), which is
|
||||
// really bad. The result will be a 3-second delay in starting each new client.
|
||||
// If you see this on a device, make certain the driver notifies with
|
||||
// {@link FingerprintManager#FINGERPRINT_ERROR_CANCEL} in response to cancel()
|
||||
// once it has successfully switched to the IDLE state in the fingerprint HAL.
|
||||
// Additionally,{@link FingerprintManager#FINGERPRINT_ERROR_CANCEL} should only be sent
|
||||
// in response to an actual cancel() call.
|
||||
Slog.w(TAG, "Client "
|
||||
+ (mCurrentClient != null ? mCurrentClient.getOwnerString() : "null")
|
||||
+ " failed to respond to cancel, starting client "
|
||||
+ (mPendingClient != null ? mPendingClient.getOwnerString() : "null"));
|
||||
mCurrentClient = null;
|
||||
startClient(mPendingClient, false);
|
||||
}
|
||||
};
|
||||
|
||||
public FingerprintService(Context context) {
|
||||
super(context);
|
||||
mContext = context;
|
||||
@@ -203,64 +216,49 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
// TODO: update fingerprint/name pairs
|
||||
}
|
||||
|
||||
protected void handleRemoved(long deviceId, int fingerId, int groupId) {
|
||||
final ClientMonitor client = mRemoveClient;
|
||||
if (fingerId != 0) {
|
||||
removeTemplateForUser(mUserIdForRemove, fingerId);
|
||||
} else {
|
||||
mUserIdForRemove = UserHandle.USER_NULL;
|
||||
protected void handleError(long deviceId, int error) {
|
||||
ClientMonitor client = mCurrentClient;
|
||||
if (client != null && client.onError(error)) {
|
||||
removeClient(client);
|
||||
}
|
||||
if (client != null && client.sendRemoved(fingerId, groupId)) {
|
||||
if (DEBUG) Slog.v(TAG, "handleError(client="
|
||||
+ client != null ? client.getOwnerString() : "null" + ", error = " + error + ")");
|
||||
// This is the magic code that starts the next client when the old client finishes.
|
||||
if (error == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
|
||||
mHandler.removeCallbacks(mResetClientState);
|
||||
if (mPendingClient != null) {
|
||||
if (DEBUG) Slog.v(TAG, "start pending client " + mPendingClient.getOwnerString());
|
||||
startClient(mPendingClient, false);
|
||||
mPendingClient = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleRemoved(long deviceId, int fingerId, int groupId) {
|
||||
ClientMonitor client = mCurrentClient;
|
||||
if (client != null && client.onRemoved(fingerId, groupId)) {
|
||||
removeClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleError(long deviceId, int error) {
|
||||
if (mEnrollClient != null) {
|
||||
final IBinder token = mEnrollClient.token;
|
||||
if (mEnrollClient.sendError(error)) {
|
||||
stopEnrollment(token, false);
|
||||
}
|
||||
} else if (mAuthClient != null) {
|
||||
final IBinder token = mAuthClient.token;
|
||||
if (mAuthClient.sendError(error)) {
|
||||
stopAuthentication(token, false);
|
||||
}
|
||||
} else if (mRemoveClient != null) {
|
||||
if (mRemoveClient.sendError(error)) removeClient(mRemoveClient);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleAuthenticated(long deviceId, int fingerId, int groupId) {
|
||||
if (mAuthClient != null) {
|
||||
final IBinder token = mAuthClient.token;
|
||||
if (mAuthClient.sendAuthenticated(fingerId, groupId)) {
|
||||
stopAuthentication(token, false);
|
||||
removeClient(mAuthClient);
|
||||
}
|
||||
ClientMonitor client = mCurrentClient;
|
||||
if (client != null && client.onAuthenticated(fingerId, groupId)) {
|
||||
removeClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleAcquired(long deviceId, int acquiredInfo) {
|
||||
if (mEnrollClient != null) {
|
||||
if (mEnrollClient.sendAcquired(acquiredInfo)) {
|
||||
removeClient(mEnrollClient);
|
||||
}
|
||||
} else if (mAuthClient != null) {
|
||||
if (mAuthClient.sendAcquired(acquiredInfo)) {
|
||||
removeClient(mAuthClient);
|
||||
}
|
||||
ClientMonitor client = mCurrentClient;
|
||||
if (client != null && client.onAcquired(acquiredInfo)) {
|
||||
removeClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleEnrollResult(long deviceId, int fingerId, int groupId, int remaining) {
|
||||
if (mEnrollClient != null) {
|
||||
if (mEnrollClient.sendEnrollResult(fingerId, groupId, remaining)) {
|
||||
if (remaining == 0) {
|
||||
addTemplateForUser(mEnrollClient, fingerId);
|
||||
removeClient(mEnrollClient);
|
||||
}
|
||||
}
|
||||
ClientMonitor client = mCurrentClient;
|
||||
if (client != null && client.onEnrollResult(fingerId, groupId, remaining)) {
|
||||
removeClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,14 +272,16 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
}
|
||||
|
||||
private void removeClient(ClientMonitor client) {
|
||||
if (client == null) return;
|
||||
client.destroy();
|
||||
if (client == mAuthClient) {
|
||||
mAuthClient = null;
|
||||
} else if (client == mEnrollClient) {
|
||||
mEnrollClient = null;
|
||||
} else if (client == mRemoveClient) {
|
||||
mRemoveClient = null;
|
||||
if (client != null) {
|
||||
client.destroy();
|
||||
if (client != mCurrentClient && mCurrentClient != null) {
|
||||
Slog.w(TAG, "Unexpected client: " + client.getOwnerString() + "expected: "
|
||||
+ mCurrentClient != null ? mCurrentClient.getOwnerString() : "null");
|
||||
}
|
||||
}
|
||||
if (mCurrentClient != null) {
|
||||
if (DEBUG) Slog.v(TAG, "Done with client: " + client.getOwnerString());
|
||||
mCurrentClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,60 +303,6 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
new Intent(ACTION_LOCKOUT_RESET), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private void resetFailedAttempts() {
|
||||
if (DEBUG && inLockoutMode()) {
|
||||
Slog.v(TAG, "Reset fingerprint lockout");
|
||||
}
|
||||
mFailedAttempts = 0;
|
||||
// If we're asked to reset failed attempts externally (i.e. from Keyguard), the alarm might
|
||||
// still be pending; remove it.
|
||||
cancelLockoutReset();
|
||||
notifyLockoutResetMonitors();
|
||||
}
|
||||
|
||||
private boolean handleFailedAttempt(ClientMonitor clientMonitor) {
|
||||
mFailedAttempts++;
|
||||
if (inLockoutMode()) {
|
||||
// Failing multiple times will continue to push out the lockout time.
|
||||
scheduleLockoutReset();
|
||||
if (clientMonitor != null
|
||||
&& !clientMonitor.sendError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT)) {
|
||||
Slog.w(TAG, "Cannot send lockout message to client");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void removeTemplateForUser(int userId, int fingerId) {
|
||||
mFingerprintUtils.removeFingerprintIdForUser(mContext, fingerId, userId);
|
||||
}
|
||||
|
||||
private void addTemplateForUser(ClientMonitor clientMonitor, int fingerId) {
|
||||
mFingerprintUtils.addFingerprintForUser(mContext, fingerId, clientMonitor.userId);
|
||||
}
|
||||
|
||||
void startEnrollment(IBinder token, byte[] cryptoToken, int groupId,
|
||||
IFingerprintServiceReceiver receiver, int flags, boolean restricted) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "enroll: no fingeprintd!");
|
||||
return;
|
||||
}
|
||||
stopPendingOperations(true);
|
||||
mEnrollClient = new ClientMonitor(token, receiver, groupId, restricted, token.toString());
|
||||
final int timeout = (int) (ENROLLMENT_TIMEOUT_MS / MS_PER_SEC);
|
||||
try {
|
||||
final int result = daemon.enroll(cryptoToken, groupId, timeout);
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "startEnroll failed, result=" + result);
|
||||
handleError(mHalDeviceId, FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "startEnroll failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public long startPreEnroll(IBinder token) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
@@ -385,123 +331,52 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void stopPendingOperations(boolean initiatedByClient) {
|
||||
if (mEnrollClient != null) {
|
||||
stopEnrollment(mEnrollClient.token, initiatedByClient);
|
||||
}
|
||||
if (mAuthClient != null) {
|
||||
stopAuthentication(mAuthClient.token, initiatedByClient);
|
||||
}
|
||||
// mRemoveClient is allowed to continue
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop enrollment in progress and inform client if they initiated it.
|
||||
*
|
||||
* @param token token for client
|
||||
* @param initiatedByClient if this call is the result of client action (e.g. calling cancel)
|
||||
* Calls fingerprintd to switch states to the new task. If there's already a current task,
|
||||
* it calls cancel() and sets mPendingClient to begin when the current task finishes
|
||||
* ({@link FingerprintManager#FINGERPRINT_ERROR_CANCELED}).
|
||||
* @param newClient the new client that wants to connect
|
||||
* @param initiatedByClient true for authenticate, remove and enroll
|
||||
*/
|
||||
void stopEnrollment(IBinder token, boolean initiatedByClient) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "stopEnrollment: no fingeprintd!");
|
||||
return;
|
||||
}
|
||||
final ClientMonitor client = mEnrollClient;
|
||||
if (client == null || client.token != token) return;
|
||||
if (initiatedByClient) {
|
||||
try {
|
||||
int result = daemon.cancelEnrollment();
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "startEnrollCancel failed, result = " + result);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "stopEnrollment failed", e);
|
||||
}
|
||||
client.sendError(FingerprintManager.FINGERPRINT_ERROR_CANCELED);
|
||||
}
|
||||
removeClient(mEnrollClient);
|
||||
}
|
||||
|
||||
void startAuthentication(IBinder token, long opId, int realUserId, int groupId,
|
||||
IFingerprintServiceReceiver receiver, int flags, boolean restricted,
|
||||
String opPackageName) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "startAuthentication: no fingeprintd!");
|
||||
return;
|
||||
}
|
||||
stopPendingOperations(true);
|
||||
updateActiveGroup(groupId, opPackageName);
|
||||
mAuthClient = new ClientMonitor(token, receiver, groupId, restricted, opPackageName);
|
||||
if (inLockoutMode()) {
|
||||
Slog.v(TAG, "In lockout mode; disallowing authentication");
|
||||
if (!mAuthClient.sendError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT)) {
|
||||
Slog.w(TAG, "Cannot send timeout message to client");
|
||||
}
|
||||
mAuthClient = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final int result = daemon.authenticate(opId, groupId);
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "startAuthentication failed, result=" + result);
|
||||
handleError(mHalDeviceId, FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "startAuthentication failed", e);
|
||||
private void startClient(ClientMonitor newClient, boolean initiatedByClient) {
|
||||
ClientMonitor currentClient = mCurrentClient;
|
||||
if (currentClient != null) {
|
||||
if (DEBUG) Slog.v(TAG, "request stop current client " + currentClient.getOwnerString());
|
||||
currentClient.stop(initiatedByClient);
|
||||
mPendingClient = newClient;
|
||||
mHandler.removeCallbacks(mResetClientState);
|
||||
mHandler.postDelayed(mResetClientState, CANCEL_TIMEOUT_LIMIT);
|
||||
} else if (newClient != null) {
|
||||
mCurrentClient = newClient;
|
||||
if (DEBUG) Slog.v(TAG, "starting client "
|
||||
+ newClient.getClass().getSuperclass().getSimpleName()
|
||||
+ "(" + newClient.getOwnerString() + ")"
|
||||
+ ", initiatedByClient = " + initiatedByClient + ")");
|
||||
newClient.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop authentication in progress and inform client if they initiated it.
|
||||
*
|
||||
* @param token token for client
|
||||
* @param initiatedByClient if this call is the result of client action (e.g. calling cancel)
|
||||
*/
|
||||
void stopAuthentication(IBinder token, boolean initiatedByClient) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "stopAuthentication: no fingeprintd!");
|
||||
return;
|
||||
}
|
||||
final ClientMonitor client = mAuthClient;
|
||||
if (client == null || client.token != token) return;
|
||||
if (initiatedByClient) {
|
||||
try {
|
||||
int result = daemon.cancelAuthentication();
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "stopAuthentication failed, result=" + result);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "stopAuthentication failed", e);
|
||||
}
|
||||
client.sendError(FingerprintManager.FINGERPRINT_ERROR_CANCELED);
|
||||
}
|
||||
removeClient(mAuthClient);
|
||||
}
|
||||
|
||||
void startRemove(IBinder token, int fingerId, int userId,
|
||||
void startRemove(IBinder token, int fingerId, int userId, int groupId,
|
||||
IFingerprintServiceReceiver receiver, boolean restricted) {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon == null) {
|
||||
Slog.w(TAG, "startRemove: no fingeprintd!");
|
||||
return;
|
||||
}
|
||||
|
||||
stopPendingOperations(true);
|
||||
mRemoveClient = new ClientMonitor(token, receiver, userId, restricted, token.toString());
|
||||
mUserIdForRemove = mCurrentUserId;
|
||||
// The fingerprint template ids will be removed when we get confirmation from the HAL
|
||||
try {
|
||||
final int result = daemon.remove(fingerId, userId);
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "startRemove with id = " + fingerId + " failed, result=" + result);
|
||||
handleError(mHalDeviceId, FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
RemovalClient client = new RemovalClient(getContext(), mHalDeviceId, token,
|
||||
receiver, userId, groupId, fingerId, restricted, token.toString()) {
|
||||
@Override
|
||||
public void notifyUserActivity() {
|
||||
FingerprintService.this.userActivity();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "startRemove failed", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IFingerprintDaemon getFingerprintDaemon() {
|
||||
FingerprintService.this.getFingerprintDaemon();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
startClient(client, true);
|
||||
}
|
||||
|
||||
public List<Fingerprint> getEnrolledFingerprints(int userId) {
|
||||
@@ -572,10 +447,9 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
* @param foregroundOnly only allow this call while app is in the foreground
|
||||
* @return true if caller can use fingerprint API
|
||||
*/
|
||||
private boolean canUseFingerprint(String opPackageName, boolean foregroundOnly) {
|
||||
private boolean canUseFingerprint(String opPackageName, boolean foregroundOnly, int uid,
|
||||
int pid) {
|
||||
checkPermission(USE_FINGERPRINT);
|
||||
final int uid = Binder.getCallingUid();
|
||||
final int pid = Binder.getCallingPid();
|
||||
if (isKeyguard(opPackageName)) {
|
||||
return true; // Keyguard is always allowed
|
||||
}
|
||||
@@ -620,165 +494,83 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
}
|
||||
}
|
||||
|
||||
private class ClientMonitor implements IBinder.DeathRecipient {
|
||||
IBinder token;
|
||||
IFingerprintServiceReceiver receiver;
|
||||
int userId;
|
||||
boolean restricted; // True if client does not have MANAGE_FINGERPRINT permission
|
||||
String owner;
|
||||
private void startAuthentication(IBinder token, long opId, int realUserId, int groupId,
|
||||
IFingerprintServiceReceiver receiver, int flags, boolean restricted,
|
||||
String opPackageName) {
|
||||
updateActiveGroup(groupId, opPackageName);
|
||||
|
||||
public ClientMonitor(IBinder token, IFingerprintServiceReceiver receiver, int userId,
|
||||
boolean restricted, String owner) {
|
||||
this.token = token;
|
||||
this.receiver = receiver;
|
||||
this.userId = userId;
|
||||
this.restricted = restricted;
|
||||
this.owner = owner; // name of the client that owns this - for debugging
|
||||
try {
|
||||
token.linkToDeath(this, 0);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "caught remote exception in linkToDeath: ", e);
|
||||
}
|
||||
}
|
||||
if (DEBUG) Slog.v(TAG, "startAuthentication(" + opPackageName + ")");
|
||||
|
||||
public void destroy() {
|
||||
if (token != null) {
|
||||
try {
|
||||
token.unlinkToDeath(this, 0);
|
||||
} catch (NoSuchElementException e) {
|
||||
// TODO: remove when duplicate call bug is found
|
||||
Slog.e(TAG, "destroy(): " + this + ":", new Exception("here"));
|
||||
AuthenticationClient client = new AuthenticationClient(getContext(), mHalDeviceId, token,
|
||||
receiver, realUserId, groupId, opId, restricted, opPackageName) {
|
||||
@Override
|
||||
public boolean handleFailedAttempt() {
|
||||
mFailedAttempts++;
|
||||
if (inLockoutMode()) {
|
||||
// Failing multiple times will continue to push out the lockout time.
|
||||
scheduleLockoutReset();
|
||||
return true;
|
||||
}
|
||||
token = null;
|
||||
return false;
|
||||
}
|
||||
receiver = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
token = null;
|
||||
removeClient(this);
|
||||
receiver = null;
|
||||
}
|
||||
@Override
|
||||
public void resetFailedAttempts() {
|
||||
FingerprintService.this.resetFailedAttempts();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
try {
|
||||
if (token != null) {
|
||||
if (DEBUG) Slog.w(TAG, "removing leaked reference: " + token);
|
||||
removeClient(this);
|
||||
}
|
||||
} finally {
|
||||
super.finalize();
|
||||
@Override
|
||||
public void notifyUserActivity() {
|
||||
FingerprintService.this.userActivity();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @return true if we're done.
|
||||
*/
|
||||
private boolean sendRemoved(int fingerId, int groupId) {
|
||||
if (receiver == null) return true; // client not listening
|
||||
try {
|
||||
receiver.onRemoved(mHalDeviceId, fingerId, groupId);
|
||||
return fingerId == 0;
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to notify Removed:", e);
|
||||
@Override
|
||||
public IFingerprintDaemon getFingerprintDaemon() {
|
||||
return FingerprintService.this.getFingerprintDaemon();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* @return true if we're done.
|
||||
*/
|
||||
private boolean sendEnrollResult(int fpId, int groupId, int remaining) {
|
||||
if (receiver == null) return true; // client not listening
|
||||
FingerprintUtils.vibrateFingerprintSuccess(getContext());
|
||||
MetricsLogger.action(mContext, MetricsEvent.ACTION_FINGERPRINT_ENROLL);
|
||||
try {
|
||||
receiver.onEnrollResult(mHalDeviceId, fpId, groupId, remaining);
|
||||
return remaining == 0;
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to notify EnrollResult:", e);
|
||||
return true;
|
||||
if (inLockoutMode()) {
|
||||
Slog.v(TAG, "In lockout mode; disallowing authentication");
|
||||
// Don't bother starting the client. Just send the error message.
|
||||
if (!client.onError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT)) {
|
||||
Slog.w(TAG, "Cannot send timeout message to client");
|
||||
}
|
||||
return;
|
||||
}
|
||||
startClient(client, true /* initiatedByClient */);
|
||||
}
|
||||
|
||||
/*
|
||||
* @return true if we're done.
|
||||
*/
|
||||
private boolean sendAuthenticated(int fpId, int groupId) {
|
||||
boolean result = false;
|
||||
boolean authenticated = fpId != 0;
|
||||
if (receiver != null) {
|
||||
try {
|
||||
MetricsLogger.action(mContext, MetricsEvent.ACTION_FINGERPRINT_AUTH,
|
||||
authenticated);
|
||||
if (!authenticated) {
|
||||
receiver.onAuthenticationFailed(mHalDeviceId);
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Slog.v(TAG, "onAuthenticated(owner=" + mAuthClient.owner
|
||||
+ ", id=" + fpId + ", gp=" + groupId + ")");
|
||||
}
|
||||
Fingerprint fp = !restricted ?
|
||||
new Fingerprint("" /* TODO */, groupId, fpId, mHalDeviceId) : null;
|
||||
receiver.onAuthenticationSucceeded(mHalDeviceId, fp);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to notify Authenticated:", e);
|
||||
result = true; // client failed
|
||||
}
|
||||
} else {
|
||||
result = true; // client not listening
|
||||
}
|
||||
if (fpId == 0) {
|
||||
if (receiver != null) {
|
||||
FingerprintUtils.vibrateFingerprintError(getContext());
|
||||
}
|
||||
result |= handleFailedAttempt(this);
|
||||
} else {
|
||||
if (receiver != null) {
|
||||
FingerprintUtils.vibrateFingerprintSuccess(getContext());
|
||||
}
|
||||
result |= true; // we have a valid fingerprint
|
||||
resetFailedAttempts();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private void startEnrollment(IBinder token, byte [] cryptoToken, int userId, int groupId,
|
||||
IFingerprintServiceReceiver receiver, int flags, boolean restricted,
|
||||
String opPackageName) {
|
||||
updateActiveGroup(groupId, opPackageName);
|
||||
|
||||
/*
|
||||
* @return true if we're done.
|
||||
*/
|
||||
private boolean sendAcquired(int acquiredInfo) {
|
||||
if (receiver == null) return true; // client not listening
|
||||
try {
|
||||
receiver.onAcquired(mHalDeviceId, acquiredInfo);
|
||||
return false; // acquisition continues...
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to invoke sendAcquired:", e);
|
||||
return true; // client failed
|
||||
}
|
||||
finally {
|
||||
// Good scans will keep the device awake
|
||||
if (acquiredInfo == FINGERPRINT_ACQUIRED_GOOD) {
|
||||
userActivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
EnrollClient client = new EnrollClient(getContext(), mHalDeviceId, token, receiver,
|
||||
userId, groupId, cryptoToken, restricted, opPackageName) {
|
||||
|
||||
/*
|
||||
* @return true if we're done.
|
||||
*/
|
||||
private boolean sendError(int error) {
|
||||
if (receiver != null) {
|
||||
try {
|
||||
receiver.onError(mHalDeviceId, error);
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to invoke sendError:", e);
|
||||
}
|
||||
@Override
|
||||
public IFingerprintDaemon getFingerprintDaemon() {
|
||||
return FingerprintService.this.getFingerprintDaemon();
|
||||
}
|
||||
return true; // errors always terminate progress
|
||||
|
||||
@Override
|
||||
public void notifyUserActivity() {
|
||||
FingerprintService.this.userActivity();
|
||||
}
|
||||
};
|
||||
startClient(client, true /* initiatedByClient */);
|
||||
}
|
||||
|
||||
protected void resetFailedAttempts() {
|
||||
if (DEBUG && inLockoutMode()) {
|
||||
Slog.v(TAG, "Reset fingerprint lockout");
|
||||
}
|
||||
mFailedAttempts = 0;
|
||||
// If we're asked to reset failed attempts externally (i.e. from Keyguard),
|
||||
// the alarm might still be pending; remove it.
|
||||
cancelLockoutReset();
|
||||
notifyLockoutResetMonitors();
|
||||
}
|
||||
|
||||
private class FingerprintServiceLockoutResetMonitor {
|
||||
@@ -876,8 +668,6 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
};
|
||||
|
||||
private final class FingerprintServiceWrapper extends IFingerprintService.Stub {
|
||||
private static final String KEYGUARD_PACKAGE = "com.android.systemui";
|
||||
|
||||
@Override // Binder call
|
||||
public long preEnroll(IBinder token) {
|
||||
checkPermission(MANAGE_FINGERPRINT);
|
||||
@@ -892,7 +682,8 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
|
||||
@Override // Binder call
|
||||
public void enroll(final IBinder token, final byte[] cryptoToken, final int groupId,
|
||||
final IFingerprintServiceReceiver receiver, final int flags) {
|
||||
final IFingerprintServiceReceiver receiver, final int flags,
|
||||
final String opPackageName) {
|
||||
checkPermission(MANAGE_FINGERPRINT);
|
||||
final int limit = mContext.getResources().getInteger(
|
||||
com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser);
|
||||
@@ -903,7 +694,6 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
Slog.w(TAG, "Too many fingerprints registered");
|
||||
return;
|
||||
}
|
||||
final byte [] cryptoClone = Arrays.copyOf(cryptoToken, cryptoToken.length);
|
||||
|
||||
// Group ID is arbitrarily set to parent profile user ID. It just represents
|
||||
// the default fingerprints for the user.
|
||||
@@ -915,7 +705,8 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
startEnrollment(token, cryptoClone, groupId, receiver, flags, restricted);
|
||||
startEnrollment(token, cryptoToken, userId, groupId, receiver, flags,
|
||||
restricted, opPackageName);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -932,7 +723,10 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
stopEnrollment(token, true);
|
||||
ClientMonitor client = mCurrentClient;
|
||||
if (client instanceof EnrollClient && client.getToken() == token) {
|
||||
client.stop(client.getToken() == token);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -941,17 +735,18 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
public void authenticate(final IBinder token, final long opId, final int groupId,
|
||||
final IFingerprintServiceReceiver receiver, final int flags,
|
||||
final String opPackageName) {
|
||||
if (!canUseFingerprint(opPackageName, true /* foregroundOnly */)) {
|
||||
if (DEBUG) Slog.v(TAG, "authenticate(): reject " + opPackageName);
|
||||
return;
|
||||
}
|
||||
final int realUserId = Binder.getCallingUid();
|
||||
|
||||
final int pid = Binder.getCallingPid();
|
||||
final boolean restricted = isRestricted();
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
MetricsLogger.histogram(mContext, "fingerprint_token", opId != 0L ? 1 : 0);
|
||||
if (!canUseFingerprint(opPackageName, true /* foregroundOnly */,
|
||||
realUserId, pid)) {
|
||||
if (DEBUG) Slog.v(TAG, "authenticate(): reject " + opPackageName);
|
||||
return;
|
||||
}
|
||||
startAuthentication(token, opId, realUserId, groupId, receiver,
|
||||
flags, restricted, opPackageName);
|
||||
}
|
||||
@@ -959,14 +754,29 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
}
|
||||
|
||||
@Override // Binder call
|
||||
public void cancelAuthentication(final IBinder token, String opPackageName) {
|
||||
if (!canUseFingerprint(opPackageName, false /* foregroundOnly */)) {
|
||||
return;
|
||||
}
|
||||
public void cancelAuthentication(final IBinder token, final String opPackageName) {
|
||||
final int uid = Binder.getCallingUid();
|
||||
final int pid = Binder.getCallingPid();
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
stopAuthentication(token, true);
|
||||
if (!canUseFingerprint(opPackageName, true /* foregroundOnly */, uid, pid)) {
|
||||
if (DEBUG) Slog.v(TAG, "cancelAuthentication(): reject " + opPackageName);
|
||||
} else {
|
||||
ClientMonitor client = mCurrentClient;
|
||||
if (client instanceof AuthenticationClient) {
|
||||
if (client.getToken() == token) {
|
||||
if (DEBUG) Slog.v(TAG, "stop client " + client.getOwnerString());
|
||||
client.stop(client.getToken() == token);
|
||||
} else {
|
||||
if (DEBUG) Slog.v(TAG, "can't stop client "
|
||||
+ client.getOwnerString() + " since tokens don't match");
|
||||
}
|
||||
} else if (client != null) {
|
||||
if (DEBUG) Slog.v(TAG, "can't cancel non-authenticating client "
|
||||
+ client.getOwnerString());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -987,10 +797,11 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
final IFingerprintServiceReceiver receiver) {
|
||||
checkPermission(MANAGE_FINGERPRINT); // TODO: Maybe have another permission
|
||||
final boolean restricted = isRestricted();
|
||||
final int realUserId = Binder.getCallingUid();
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
startRemove(token, fingerId, groupId, receiver, restricted);
|
||||
startRemove(token, fingerId, realUserId, groupId, receiver, restricted);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -998,7 +809,8 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
|
||||
@Override // Binder call
|
||||
public boolean isHardwareDetected(long deviceId, String opPackageName) {
|
||||
if (!canUseFingerprint(opPackageName, false /* foregroundOnly */)) {
|
||||
if (!canUseFingerprint(opPackageName, false /* foregroundOnly */,
|
||||
Binder.getCallingUid(), Binder.getCallingPid())) {
|
||||
return false;
|
||||
}
|
||||
return mHalDeviceId != 0;
|
||||
@@ -1021,7 +833,8 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
|
||||
@Override // Binder call
|
||||
public List<Fingerprint> getEnrolledFingerprints(int userId, String opPackageName) {
|
||||
if (!canUseFingerprint(opPackageName, false /* foregroundOnly */)) {
|
||||
if (!canUseFingerprint(opPackageName, false /* foregroundOnly */,
|
||||
Binder.getCallingUid(), Binder.getCallingPid())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (!isCurrentUserOrProfile(userId)) {
|
||||
@@ -1033,7 +846,8 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
|
||||
@Override // Binder call
|
||||
public boolean hasEnrolledFingerprints(int userId, String opPackageName) {
|
||||
if (!canUseFingerprint(opPackageName, false /* foregroundOnly */)) {
|
||||
if (!canUseFingerprint(opPackageName, false /* foregroundOnly */,
|
||||
Binder.getCallingUid(), Binder.getCallingPid())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1061,7 +875,7 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
// The permission check should be restored once Android Keystore no longer invokes this
|
||||
// method from inside app processes.
|
||||
|
||||
return FingerprintService.this.getAuthenticatorId();
|
||||
return FingerprintService.this.getAuthenticatorId(opPackageName);
|
||||
}
|
||||
|
||||
@Override // Binder call
|
||||
@@ -1154,6 +968,7 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
}
|
||||
daemon.setActiveGroup(userId, fpDir.getAbsolutePath().getBytes());
|
||||
mCurrentUserId = userId;
|
||||
mCurrentAuthenticatorId = daemon.getAuthenticatorId();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "Failed to setActiveGroup():", e);
|
||||
@@ -1204,14 +1019,12 @@ public class FingerprintService extends SystemService implements IBinder.DeathRe
|
||||
}
|
||||
}
|
||||
|
||||
public long getAuthenticatorId() {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
if (daemon != null) {
|
||||
try {
|
||||
return daemon.getAuthenticatorId();
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "getAuthenticatorId failed", e);
|
||||
}
|
||||
public long getAuthenticatorId(String opPackageName) {
|
||||
if (canUseFingerprint(opPackageName, false /* foregroundOnly */,
|
||||
Binder.getCallingUid(), Binder.getCallingPid())) {
|
||||
return mCurrentAuthenticatorId;
|
||||
} else {
|
||||
Slog.w(TAG, "Client isn't current, returning authenticator_id=0");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (C) 2016 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.server.fingerprint;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.hardware.fingerprint.IFingerprintDaemon;
|
||||
import android.hardware.fingerprint.IFingerprintServiceReceiver;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.util.Slog;
|
||||
|
||||
/**
|
||||
* A class to keep track of the remove state for a given client.
|
||||
*/
|
||||
public abstract class RemovalClient extends ClientMonitor {
|
||||
private int mFingerId;
|
||||
private int mUserIdForRemove;
|
||||
|
||||
public RemovalClient(Context context, long halDeviceId, IBinder token,
|
||||
IFingerprintServiceReceiver receiver, int userId, int groupId, int fingerId,
|
||||
boolean restricted, String owner) {
|
||||
super(context, halDeviceId, token, receiver, userId, groupId, restricted, owner);
|
||||
mFingerId = fingerId;
|
||||
mUserIdForRemove = userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int start() {
|
||||
IFingerprintDaemon daemon = getFingerprintDaemon();
|
||||
// The fingerprint template ids will be removed when we get confirmation from the HAL
|
||||
try {
|
||||
final int result = daemon.remove(mFingerId, getUserId());
|
||||
if (result != 0) {
|
||||
Slog.w(TAG, "startRemove with id = " + mFingerId + " failed, result=" + result);
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE);
|
||||
return result;
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "startRemove failed", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int stop(boolean initiatedByClient) {
|
||||
// We don't actually stop remove, but inform the client that the cancel operation succeeded
|
||||
// so we can start the next operation.
|
||||
if (initiatedByClient) {
|
||||
onError(FingerprintManager.FINGERPRINT_ERROR_CANCELED);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return true if we're done.
|
||||
*/
|
||||
private boolean sendRemoved(int fingerId, int groupId) {
|
||||
IFingerprintServiceReceiver receiver = getReceiver();
|
||||
if (receiver == null)
|
||||
return true; // client not listening
|
||||
try {
|
||||
receiver.onRemoved(getHalDeviceId(), fingerId, groupId);
|
||||
return fingerId == 0;
|
||||
} catch (RemoteException e) {
|
||||
Slog.w(TAG, "Failed to notify Removed:", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRemoved(int fingerId, int groupId) {
|
||||
if (fingerId != 0) {
|
||||
if (fingerId != mFingerId)
|
||||
FingerprintUtils.getInstance().removeFingerprintIdForUser(getContext(), fingerId,
|
||||
mUserIdForRemove);
|
||||
} else {
|
||||
mUserIdForRemove = UserHandle.USER_NULL;
|
||||
}
|
||||
return sendRemoved(fingerId, getGroupId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEnrollResult(int fingerId, int groupId, int rem) {
|
||||
if (DEBUG) Slog.w(TAG, "onEnrollResult() called for remove!");
|
||||
return true; // Invalid for Remove
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAuthenticated(int fingerId, int groupId) {
|
||||
if (DEBUG) Slog.w(TAG, "onAuthenticated() called for remove!");
|
||||
return true; // Invalid for Remove.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEnumerationResult(int fingerId, int groupId) {
|
||||
if (DEBUG) Slog.w(TAG, "onEnumerationResult() called for remove!");
|
||||
return false; // Invalid for Remove.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user