[2/n] FRR data persistence

Add AuthenticationStatsPersister to store on-device FRR
data.

Bug: 258872351
Test: atest AuthenticationStatsPersisterTest
Test: atest AuthenticationStatsCollectorTest
Change-Id: I02e61e12595151ceeee0d4a92e79d95cfa416585
This commit is contained in:
Wenhui Yang
2023-07-24 19:33:02 +00:00
parent a0224693b0
commit 53bc9fa614
5 changed files with 508 additions and 15 deletions

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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 "";
}
}