Merge "[2/n] FRR data persistence" into udc-qpr-dev
This commit is contained in:
@@ -22,6 +22,8 @@ package com.android.server.biometrics;
|
||||
*/
|
||||
public class AuthenticationStats {
|
||||
|
||||
private static final float FRR_NOT_ENOUGH_ATTEMPTS = -1.0f;
|
||||
|
||||
private final int mUserId;
|
||||
private int mTotalAttempts;
|
||||
private int mRejectedAttempts;
|
||||
@@ -70,7 +72,7 @@ public class AuthenticationStats {
|
||||
if (mTotalAttempts > 0) {
|
||||
return mRejectedAttempts / (float) mTotalAttempts;
|
||||
} else {
|
||||
return -1.0f;
|
||||
return FRR_NOT_ENOUGH_ATTEMPTS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,4 +89,32 @@ public class AuthenticationStats {
|
||||
mTotalAttempts = 0;
|
||||
mRejectedAttempts = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof AuthenticationStats)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthenticationStats target = (AuthenticationStats) obj;
|
||||
return this.getUserId() == target.getUserId()
|
||||
&& this.getTotalAttempts()
|
||||
== target.getTotalAttempts()
|
||||
&& this.getRejectedAttempts()
|
||||
== target.getRejectedAttempts()
|
||||
&& this.getEnrollmentNotifications()
|
||||
== target.getEnrollmentNotifications()
|
||||
&& this.getModality() == target.getModality();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return String.format("userId: %d, totalAttempts: %d, rejectedAttempts: %d, "
|
||||
+ "enrollmentNotifications: %d, modality: %d", mUserId, mTotalAttempts,
|
||||
mRejectedAttempts, mEnrollmentNotifications, mModality).hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,23 +37,42 @@ public class AuthenticationStatsCollector {
|
||||
|
||||
// The minimum number of attempts that will calculate the FRR and trigger the notification.
|
||||
private static final int MINIMUM_ATTEMPTS = 500;
|
||||
// Upload the data every 50 attempts (average number of daily authentications).
|
||||
private static final int AUTHENTICATION_UPLOAD_INTERVAL = 50;
|
||||
// The maximum number of eligible biometric enrollment notification can be sent.
|
||||
private static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 2;
|
||||
|
||||
@NonNull private final Context mContext;
|
||||
|
||||
private final float mThreshold;
|
||||
private final int mModality;
|
||||
|
||||
@NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap;
|
||||
|
||||
@NonNull private AuthenticationStatsPersister mAuthenticationStatsPersister;
|
||||
|
||||
public AuthenticationStatsCollector(@NonNull Context context, int modality) {
|
||||
mContext = context;
|
||||
mThreshold = context.getResources()
|
||||
.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1);
|
||||
mUserAuthenticationStatsMap = new HashMap<>();
|
||||
mModality = modality;
|
||||
}
|
||||
|
||||
private void initializeUserAuthenticationStatsMap() {
|
||||
mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext);
|
||||
for (AuthenticationStats stats : mAuthenticationStatsPersister.getAllFrrStats(mModality)) {
|
||||
mUserAuthenticationStatsMap.put(stats.getUserId(), stats);
|
||||
}
|
||||
}
|
||||
|
||||
/** Update total authentication and rejected attempts. */
|
||||
public void authenticate(int userId, boolean authenticated) {
|
||||
// Initialize mUserAuthenticationStatsMap when authenticate to ensure SharedPreferences
|
||||
// are ready for application use and avoid ramdump issue.
|
||||
if (mUserAuthenticationStatsMap.isEmpty()) {
|
||||
initializeUserAuthenticationStatsMap();
|
||||
}
|
||||
// Check if this is a new user.
|
||||
if (!mUserAuthenticationStatsMap.containsKey(userId)) {
|
||||
mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality));
|
||||
@@ -82,8 +101,12 @@ public class AuthenticationStatsCollector {
|
||||
|
||||
private void persistDataIfNeeded(int userId) {
|
||||
AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
|
||||
if (authenticationStats.getTotalAttempts() % 50 == 0) {
|
||||
// TODO(wenhuiy): Persist data.
|
||||
if (authenticationStats.getTotalAttempts() % AUTHENTICATION_UPLOAD_INTERVAL == 0) {
|
||||
mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(),
|
||||
authenticationStats.getTotalAttempts(),
|
||||
authenticationStats.getRejectedAttempts(),
|
||||
authenticationStats.getEnrollmentNotifications(),
|
||||
authenticationStats.getModality());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.biometrics;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.hardware.biometrics.BiometricsProtoEnums;
|
||||
import android.os.Environment;
|
||||
import android.util.Slog;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Persists and retrieves stats for Biometric Authentication.
|
||||
* Authentication stats include userId, total attempts, rejected attempts,
|
||||
* and the number of sent enrollment notifications.
|
||||
* Data are stored in SharedPreferences in a form of a set of JSON objects,
|
||||
* where it's one element per user.
|
||||
*/
|
||||
public class AuthenticationStatsPersister {
|
||||
|
||||
private static final String TAG = "AuthenticationStatsPersister";
|
||||
private static final String FILE_NAME = "authentication_stats";
|
||||
private static final String USER_ID = "user_id";
|
||||
private static final String FACE_ATTEMPTS = "face_attempts";
|
||||
private static final String FACE_REJECTIONS = "face_rejections";
|
||||
private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts";
|
||||
private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections";
|
||||
private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications";
|
||||
private static final String KEY = "frr_stats";
|
||||
|
||||
@NonNull private final SharedPreferences mSharedPreferences;
|
||||
|
||||
AuthenticationStatsPersister(@NonNull Context context) {
|
||||
// The package info in the context isn't initialized in the way it is for normal apps,
|
||||
// so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
|
||||
// build the path manually below using the same policy that appears in ContextImpl.
|
||||
final File prefsFile = new File(Environment.getDataSystemDeDirectory(), FILE_NAME);
|
||||
mSharedPreferences = context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all frr data from SharedPreference.
|
||||
*/
|
||||
public List<AuthenticationStats> getAllFrrStats(int modality) {
|
||||
List<AuthenticationStats> authenticationStatsList = new ArrayList<>();
|
||||
for (String frrStats : readFrrStats()) {
|
||||
try {
|
||||
JSONObject frrStatsJson = new JSONObject(frrStats);
|
||||
if (modality == BiometricsProtoEnums.MODALITY_FACE) {
|
||||
authenticationStatsList.add(new AuthenticationStats(
|
||||
getIntValue(frrStatsJson, USER_ID, -1 /* defaultValue */),
|
||||
getIntValue(frrStatsJson, FACE_ATTEMPTS),
|
||||
getIntValue(frrStatsJson, FACE_REJECTIONS),
|
||||
getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS),
|
||||
modality));
|
||||
} else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
|
||||
authenticationStatsList.add(new AuthenticationStats(
|
||||
getIntValue(frrStatsJson, USER_ID, -1 /* defaultValue */),
|
||||
getIntValue(frrStatsJson, FINGERPRINT_ATTEMPTS),
|
||||
getIntValue(frrStatsJson, FINGERPRINT_REJECTIONS),
|
||||
getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS),
|
||||
modality));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Slog.w(TAG, String.format("Unable to resolve authentication stats JSON: %s",
|
||||
frrStats));
|
||||
}
|
||||
}
|
||||
return authenticationStatsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist frr data for a specific user.
|
||||
*/
|
||||
public void persistFrrStats(int userId, int totalAttempts, int rejectedAttempts,
|
||||
int enrollmentNotifications, int modality) {
|
||||
try {
|
||||
// Copy into a new HashSet to avoid iterator exception.
|
||||
Set<String> frrStatsSet = new HashSet<>(readFrrStats());
|
||||
|
||||
// Remove the old authentication stat for the user if it exists.
|
||||
JSONObject frrStatJson = null;
|
||||
for (Iterator<String> iterator = frrStatsSet.iterator(); iterator.hasNext();) {
|
||||
String frrStats = iterator.next();
|
||||
frrStatJson = new JSONObject(frrStats);
|
||||
if (getValue(frrStatJson, USER_ID).equals(String.valueOf(userId))) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's existing frr stats in the file, we want to update the stats for the given
|
||||
// modality and keep the stats for other modalities.
|
||||
if (frrStatJson != null) {
|
||||
frrStatsSet.add(buildFrrStats(frrStatJson, totalAttempts, rejectedAttempts,
|
||||
enrollmentNotifications, modality));
|
||||
} else {
|
||||
frrStatsSet.add(buildFrrStats(userId, totalAttempts, rejectedAttempts,
|
||||
enrollmentNotifications, modality));
|
||||
}
|
||||
|
||||
mSharedPreferences.edit().putStringSet(KEY, frrStatsSet).apply();
|
||||
|
||||
} catch (JSONException e) {
|
||||
Slog.e(TAG, "Unable to persist authentication stats");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> readFrrStats() {
|
||||
return mSharedPreferences.getStringSet(KEY, Set.of());
|
||||
}
|
||||
|
||||
// Update frr stats for existing frrStats JSONObject and build the new string.
|
||||
private String buildFrrStats(JSONObject frrStats, int totalAttempts, int rejectedAttempts,
|
||||
int enrollmentNotifications, int modality) throws JSONException {
|
||||
if (modality == BiometricsProtoEnums.MODALITY_FACE) {
|
||||
return frrStats
|
||||
.put(FACE_ATTEMPTS, totalAttempts)
|
||||
.put(FACE_REJECTIONS, rejectedAttempts)
|
||||
.put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications)
|
||||
.toString();
|
||||
} else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
|
||||
return frrStats
|
||||
.put(FINGERPRINT_ATTEMPTS, totalAttempts)
|
||||
.put(FINGERPRINT_REJECTIONS, rejectedAttempts)
|
||||
.put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications)
|
||||
.toString();
|
||||
} else {
|
||||
return frrStats.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Build string for new user and new authentication stats.
|
||||
private String buildFrrStats(int userId, int totalAttempts, int rejectedAttempts,
|
||||
int enrollmentNotifications, int modality)
|
||||
throws JSONException {
|
||||
if (modality == BiometricsProtoEnums.MODALITY_FACE) {
|
||||
return new JSONObject()
|
||||
.put(USER_ID, userId)
|
||||
.put(FACE_ATTEMPTS, totalAttempts)
|
||||
.put(FACE_REJECTIONS, rejectedAttempts)
|
||||
.put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications)
|
||||
.toString();
|
||||
} else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
|
||||
return new JSONObject()
|
||||
.put(USER_ID, userId)
|
||||
.put(FINGERPRINT_ATTEMPTS, totalAttempts)
|
||||
.put(FINGERPRINT_REJECTIONS, rejectedAttempts)
|
||||
.put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications)
|
||||
.toString();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String getValue(JSONObject jsonObject, String key) throws JSONException {
|
||||
return jsonObject.has(key) ? jsonObject.getString(key) : "";
|
||||
}
|
||||
|
||||
private int getIntValue(JSONObject jsonObject, String key) throws JSONException {
|
||||
return getIntValue(jsonObject, key, 0 /* defaultValue */);
|
||||
}
|
||||
|
||||
private int getIntValue(JSONObject jsonObject, String key, int defaultValue)
|
||||
throws JSONException {
|
||||
return jsonObject.has(key) ? jsonObject.getInt(key) : defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,18 @@
|
||||
|
||||
package com.android.server.biometrics;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anySet;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import com.android.internal.R;
|
||||
@@ -30,6 +37,8 @@ import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class AuthenticationStatsCollectorTest {
|
||||
|
||||
private AuthenticationStatsCollector mAuthenticationStatsCollector;
|
||||
@@ -40,6 +49,8 @@ public class AuthenticationStatsCollectorTest {
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private Resources mResources;
|
||||
@Mock
|
||||
private SharedPreferences mSharedPreferences;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
@@ -48,6 +59,9 @@ public class AuthenticationStatsCollectorTest {
|
||||
when(mContext.getResources()).thenReturn(mResources);
|
||||
when(mResources.getFraction(R.fraction.config_biometricNotificationFrrThreshold, 1, 1))
|
||||
.thenReturn(FRR_THRESHOLD);
|
||||
when(mContext.getSharedPreferences(any(File.class), anyInt()))
|
||||
.thenReturn(mSharedPreferences);
|
||||
when(mSharedPreferences.getStringSet(anyString(), anySet())).thenReturn(emptySet());
|
||||
|
||||
mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
|
||||
0 /* modality */);
|
||||
@@ -57,30 +71,31 @@ public class AuthenticationStatsCollectorTest {
|
||||
@Test
|
||||
public void authenticate_authenticationSucceeded_mapShouldBeUpdated() {
|
||||
// Assert that the user doesn't exist in the map initially.
|
||||
assertNull(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1));
|
||||
assertThat(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)).isNull();
|
||||
|
||||
mAuthenticationStatsCollector.authenticate(USER_ID_1, true /* authenticated*/);
|
||||
|
||||
AuthenticationStats authenticationStats =
|
||||
mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1);
|
||||
assertEquals(USER_ID_1, authenticationStats.getUserId());
|
||||
assertEquals(1, authenticationStats.getTotalAttempts());
|
||||
assertEquals(0, authenticationStats.getRejectedAttempts());
|
||||
assertEquals(0, authenticationStats.getEnrollmentNotifications());
|
||||
assertThat(authenticationStats.getUserId()).isEqualTo(USER_ID_1);
|
||||
assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1);
|
||||
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
|
||||
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticate_authenticationFailed_mapShouldBeUpdated() {
|
||||
// Assert that the user doesn't exist in the map initially.
|
||||
assertNull(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1));
|
||||
assertThat(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)).isNull();
|
||||
|
||||
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated*/);
|
||||
|
||||
AuthenticationStats authenticationStats =
|
||||
mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1);
|
||||
assertEquals(USER_ID_1, authenticationStats.getUserId());
|
||||
assertEquals(1, authenticationStats.getTotalAttempts());
|
||||
assertEquals(1, authenticationStats.getRejectedAttempts());
|
||||
assertEquals(0, authenticationStats.getEnrollmentNotifications());
|
||||
|
||||
assertThat(authenticationStats.getUserId()).isEqualTo(USER_ID_1);
|
||||
assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1);
|
||||
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(1);
|
||||
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.biometrics;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anySet;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.hardware.biometrics.BiometricsProtoEnums;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class AuthenticationStatsPersisterTest {
|
||||
|
||||
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
|
||||
|
||||
private static final int USER_ID_1 = 1;
|
||||
private static final int USER_ID_2 = 2;
|
||||
private static final String USER_ID = "user_id";
|
||||
private static final String FACE_ATTEMPTS = "face_attempts";
|
||||
private static final String FACE_REJECTIONS = "face_rejections";
|
||||
private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts";
|
||||
private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections";
|
||||
private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications";
|
||||
private static final String KEY = "frr_stats";
|
||||
|
||||
@Mock
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private SharedPreferences mSharedPreferences;
|
||||
@Mock
|
||||
private SharedPreferences.Editor mEditor;
|
||||
private AuthenticationStatsPersister mAuthenticationStatsPersister;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Set<String>> mStringSetArgumentCaptor;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
when(mContext.getSharedPreferences(any(File.class), anyInt()))
|
||||
.thenReturn(mSharedPreferences);
|
||||
when(mSharedPreferences.edit()).thenReturn(mEditor);
|
||||
when(mEditor.putStringSet(anyString(), anySet())).thenReturn(mEditor);
|
||||
|
||||
mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllFrrStats_face_shouldListAllFrrStats() throws JSONException {
|
||||
AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1,
|
||||
300 /* totalAttempts */, 10 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
|
||||
AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2,
|
||||
200 /* totalAttempts */, 20 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
|
||||
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
|
||||
Set.of(buildFrrStats(stats1), buildFrrStats(stats2)));
|
||||
|
||||
List<AuthenticationStats> authenticationStatsList =
|
||||
mAuthenticationStatsPersister.getAllFrrStats(BiometricsProtoEnums.MODALITY_FACE);
|
||||
|
||||
assertThat(authenticationStatsList.size()).isEqualTo(2);
|
||||
AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2,
|
||||
0 /* totalAttempts */, 0 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
|
||||
assertThat(authenticationStatsList).contains(stats1);
|
||||
assertThat(authenticationStatsList).contains(expectedStats2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllFrrStats_fingerprint_shouldListAllFrrStats() throws JSONException {
|
||||
// User 1 with fingerprint authentication stats.
|
||||
AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1,
|
||||
200 /* totalAttempts */, 20 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
|
||||
// User 2 without fingerprint authentication stats.
|
||||
AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2,
|
||||
300 /* totalAttempts */, 10 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
|
||||
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
|
||||
Set.of(buildFrrStats(stats1), buildFrrStats(stats2)));
|
||||
|
||||
List<AuthenticationStats> authenticationStatsList =
|
||||
mAuthenticationStatsPersister
|
||||
.getAllFrrStats(BiometricsProtoEnums.MODALITY_FINGERPRINT);
|
||||
|
||||
assertThat(authenticationStatsList.size()).isEqualTo(2);
|
||||
AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2,
|
||||
0 /* totalAttempts */, 0 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
|
||||
assertThat(authenticationStatsList).contains(stats1);
|
||||
assertThat(authenticationStatsList).contains(expectedStats2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistFrrStats_newUser_face_shouldSuccess() throws JSONException {
|
||||
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
|
||||
300 /* totalAttempts */, 10 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
|
||||
|
||||
mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(),
|
||||
authenticationStats.getTotalAttempts(),
|
||||
authenticationStats.getRejectedAttempts(),
|
||||
authenticationStats.getEnrollmentNotifications(),
|
||||
authenticationStats.getModality());
|
||||
|
||||
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
|
||||
assertThat(mStringSetArgumentCaptor.getValue())
|
||||
.contains(buildFrrStats(authenticationStats));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistFrrStats_newUser_fingerprint_shouldSuccess() throws JSONException {
|
||||
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
|
||||
300 /* totalAttempts */, 10 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
|
||||
|
||||
mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(),
|
||||
authenticationStats.getTotalAttempts(),
|
||||
authenticationStats.getRejectedAttempts(),
|
||||
authenticationStats.getEnrollmentNotifications(),
|
||||
authenticationStats.getModality());
|
||||
|
||||
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
|
||||
assertThat(mStringSetArgumentCaptor.getValue())
|
||||
.contains(buildFrrStats(authenticationStats));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistFrrStats_existingUser_shouldUpdateRecord() throws JSONException {
|
||||
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
|
||||
300 /* totalAttempts */, 10 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
|
||||
AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1,
|
||||
500 /* totalAttempts */, 30 /* rejectedAttempts */,
|
||||
1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
|
||||
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
|
||||
Set.of(buildFrrStats(authenticationStats)));
|
||||
|
||||
mAuthenticationStatsPersister.persistFrrStats(newAuthenticationStats.getUserId(),
|
||||
newAuthenticationStats.getTotalAttempts(),
|
||||
newAuthenticationStats.getRejectedAttempts(),
|
||||
newAuthenticationStats.getEnrollmentNotifications(),
|
||||
newAuthenticationStats.getModality());
|
||||
|
||||
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
|
||||
assertThat(mStringSetArgumentCaptor.getValue())
|
||||
.contains(buildFrrStats(newAuthenticationStats));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistFrrStats_existingUserWithFingerprint_faceAuthenticate_shouldUpdateRecord()
|
||||
throws JSONException {
|
||||
// User with fingerprint authentication stats.
|
||||
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
|
||||
200 /* totalAttempts */, 20 /* rejectedAttempts */,
|
||||
0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
|
||||
// The same user with face authentication stats.
|
||||
AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1,
|
||||
500 /* totalAttempts */, 30 /* rejectedAttempts */,
|
||||
1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
|
||||
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
|
||||
Set.of(buildFrrStats(authenticationStats)));
|
||||
|
||||
mAuthenticationStatsPersister.persistFrrStats(newAuthenticationStats.getUserId(),
|
||||
newAuthenticationStats.getTotalAttempts(),
|
||||
newAuthenticationStats.getRejectedAttempts(),
|
||||
newAuthenticationStats.getEnrollmentNotifications(),
|
||||
newAuthenticationStats.getModality());
|
||||
|
||||
String expectedFrrStats = new JSONObject(buildFrrStats(authenticationStats))
|
||||
.put(ENROLLMENT_NOTIFICATIONS, newAuthenticationStats.getEnrollmentNotifications())
|
||||
.put(FACE_ATTEMPTS, newAuthenticationStats.getTotalAttempts())
|
||||
.put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts()).toString();
|
||||
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
|
||||
assertThat(mStringSetArgumentCaptor.getValue()).contains(expectedFrrStats);
|
||||
}
|
||||
|
||||
private String buildFrrStats(AuthenticationStats authenticationStats)
|
||||
throws JSONException {
|
||||
if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FACE) {
|
||||
return new JSONObject()
|
||||
.put(USER_ID, authenticationStats.getUserId())
|
||||
.put(FACE_ATTEMPTS, authenticationStats.getTotalAttempts())
|
||||
.put(FACE_REJECTIONS, authenticationStats.getRejectedAttempts())
|
||||
.put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications())
|
||||
.toString();
|
||||
} else if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
|
||||
return new JSONObject()
|
||||
.put(USER_ID, authenticationStats.getUserId())
|
||||
.put(FINGERPRINT_ATTEMPTS, authenticationStats.getTotalAttempts())
|
||||
.put(FINGERPRINT_REJECTIONS, authenticationStats.getRejectedAttempts())
|
||||
.put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications())
|
||||
.toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user