1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.biometrics; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.ArgumentMatchers.anyFloat; 23 import static org.mockito.ArgumentMatchers.anyInt; 24 import static org.mockito.ArgumentMatchers.anySet; 25 import static org.mockito.ArgumentMatchers.anyString; 26 import static org.mockito.ArgumentMatchers.eq; 27 import static org.mockito.Mockito.verify; 28 import static org.mockito.Mockito.when; 29 30 import android.content.Context; 31 import android.content.SharedPreferences; 32 import android.hardware.biometrics.BiometricsProtoEnums; 33 import android.platform.test.annotations.Presubmit; 34 35 import androidx.test.filters.SmallTest; 36 37 import org.json.JSONException; 38 import org.json.JSONObject; 39 import org.junit.Before; 40 import org.junit.Rule; 41 import org.junit.Test; 42 import org.mockito.ArgumentCaptor; 43 import org.mockito.Captor; 44 import org.mockito.Mock; 45 import org.mockito.junit.MockitoJUnit; 46 import org.mockito.junit.MockitoRule; 47 48 import java.io.File; 49 import java.util.List; 50 import java.util.Set; 51 52 @Presubmit 53 @SmallTest 54 public class AuthenticationStatsPersisterTest { 55 56 @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); 57 58 private static final int USER_ID_1 = 1; 59 private static final int USER_ID_2 = 2; 60 private static final String USER_ID = "user_id"; 61 private static final String FACE_ATTEMPTS = "face_attempts"; 62 private static final String FACE_REJECTIONS = "face_rejections"; 63 private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts"; 64 private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections"; 65 private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications"; 66 private static final String KEY = "frr_stats"; 67 private static final String THRESHOLD_KEY = "frr_threshold"; 68 private static final float FRR_THRESHOLD = 0.25f; 69 70 @Mock 71 private Context mContext; 72 @Mock 73 private SharedPreferences mSharedPreferences; 74 @Mock 75 private SharedPreferences.Editor mEditor; 76 private AuthenticationStatsPersister mAuthenticationStatsPersister; 77 78 @Captor 79 private ArgumentCaptor<Set<String>> mStringSetArgumentCaptor; 80 @Captor 81 private ArgumentCaptor<Float> mFrrThresholdArgumentCaptor; 82 83 @Before setUp()84 public void setUp() { 85 when(mContext.getSharedPreferences(any(File.class), anyInt())) 86 .thenReturn(mSharedPreferences); 87 when(mSharedPreferences.edit()).thenReturn(mEditor); 88 when(mEditor.putStringSet(anyString(), anySet())).thenReturn(mEditor); 89 when(mEditor.putFloat(anyString(), anyFloat())).thenReturn(mEditor); 90 91 mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext); 92 } 93 94 @Test getAllFrrStats_face_shouldListAllFrrStats()95 public void getAllFrrStats_face_shouldListAllFrrStats() throws JSONException { 96 AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1, 97 300 /* totalAttempts */, 10 /* rejectedAttempts */, 98 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 99 AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2, 100 200 /* totalAttempts */, 20 /* rejectedAttempts */, 101 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); 102 when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( 103 Set.of(buildFrrStats(stats1), buildFrrStats(stats2))); 104 105 List<AuthenticationStats> authenticationStatsList = 106 mAuthenticationStatsPersister.getAllFrrStats(BiometricsProtoEnums.MODALITY_FACE); 107 108 assertThat(authenticationStatsList.size()).isEqualTo(2); 109 AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2, 110 0 /* totalAttempts */, 0 /* rejectedAttempts */, 111 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 112 assertThat(authenticationStatsList).contains(stats1); 113 assertThat(authenticationStatsList).contains(expectedStats2); 114 } 115 116 @Test getAllFrrStats_fingerprint_shouldListAllFrrStats()117 public void getAllFrrStats_fingerprint_shouldListAllFrrStats() throws JSONException { 118 // User 1 with fingerprint authentication stats. 119 AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1, 120 200 /* totalAttempts */, 20 /* rejectedAttempts */, 121 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); 122 // User 2 without fingerprint authentication stats. 123 AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2, 124 300 /* totalAttempts */, 10 /* rejectedAttempts */, 125 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 126 when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( 127 Set.of(buildFrrStats(stats1), buildFrrStats(stats2))); 128 129 List<AuthenticationStats> authenticationStatsList = 130 mAuthenticationStatsPersister 131 .getAllFrrStats(BiometricsProtoEnums.MODALITY_FINGERPRINT); 132 133 assertThat(authenticationStatsList.size()).isEqualTo(2); 134 AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2, 135 0 /* totalAttempts */, 0 /* rejectedAttempts */, 136 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); 137 assertThat(authenticationStatsList).contains(stats1); 138 assertThat(authenticationStatsList).contains(expectedStats2); 139 } 140 141 @Test persistFrrStats_newUser_face_shouldSuccess()142 public void persistFrrStats_newUser_face_shouldSuccess() throws JSONException { 143 AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 144 300 /* totalAttempts */, 10 /* rejectedAttempts */, 145 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 146 147 mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(), 148 authenticationStats.getTotalAttempts(), 149 authenticationStats.getRejectedAttempts(), 150 authenticationStats.getEnrollmentNotifications(), 151 authenticationStats.getModality()); 152 153 verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); 154 assertThat(mStringSetArgumentCaptor.getValue()) 155 .contains(buildFrrStats(authenticationStats)); 156 } 157 158 @Test persistFrrStats_newUser_fingerprint_shouldSuccess()159 public void persistFrrStats_newUser_fingerprint_shouldSuccess() throws JSONException { 160 AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 161 300 /* totalAttempts */, 10 /* rejectedAttempts */, 162 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); 163 164 mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(), 165 authenticationStats.getTotalAttempts(), 166 authenticationStats.getRejectedAttempts(), 167 authenticationStats.getEnrollmentNotifications(), 168 authenticationStats.getModality()); 169 170 verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); 171 assertThat(mStringSetArgumentCaptor.getValue()) 172 .contains(buildFrrStats(authenticationStats)); 173 } 174 175 @Test persistFrrStats_existingUser_shouldUpdateRecord()176 public void persistFrrStats_existingUser_shouldUpdateRecord() throws JSONException { 177 AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 178 300 /* totalAttempts */, 10 /* rejectedAttempts */, 179 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 180 AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1, 181 500 /* totalAttempts */, 30 /* rejectedAttempts */, 182 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 183 when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( 184 Set.of(buildFrrStats(authenticationStats))); 185 186 mAuthenticationStatsPersister.persistFrrStats(newAuthenticationStats.getUserId(), 187 newAuthenticationStats.getTotalAttempts(), 188 newAuthenticationStats.getRejectedAttempts(), 189 newAuthenticationStats.getEnrollmentNotifications(), 190 newAuthenticationStats.getModality()); 191 192 verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); 193 assertThat(mStringSetArgumentCaptor.getValue()) 194 .contains(buildFrrStats(newAuthenticationStats)); 195 } 196 197 @Test persistFrrStats_existingUserWithFingerprint_faceAuthenticate_shouldUpdateRecord()198 public void persistFrrStats_existingUserWithFingerprint_faceAuthenticate_shouldUpdateRecord() 199 throws JSONException { 200 // User with fingerprint authentication stats. 201 AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 202 200 /* totalAttempts */, 20 /* rejectedAttempts */, 203 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); 204 // The same user with face authentication stats. 205 AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1, 206 500 /* totalAttempts */, 30 /* rejectedAttempts */, 207 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 208 when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( 209 Set.of(buildFrrStats(authenticationStats))); 210 211 mAuthenticationStatsPersister.persistFrrStats(newAuthenticationStats.getUserId(), 212 newAuthenticationStats.getTotalAttempts(), 213 newAuthenticationStats.getRejectedAttempts(), 214 newAuthenticationStats.getEnrollmentNotifications(), 215 newAuthenticationStats.getModality()); 216 217 String expectedFrrStats = new JSONObject(buildFrrStats(authenticationStats)) 218 .put(ENROLLMENT_NOTIFICATIONS, newAuthenticationStats.getEnrollmentNotifications()) 219 .put(FACE_ATTEMPTS, newAuthenticationStats.getTotalAttempts()) 220 .put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts()).toString(); 221 verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); 222 assertThat(mStringSetArgumentCaptor.getValue()).contains(expectedFrrStats); 223 } 224 225 @Test persistFrrStats_multiUser_newUser_shouldUpdateRecord()226 public void persistFrrStats_multiUser_newUser_shouldUpdateRecord() throws JSONException { 227 AuthenticationStats authenticationStats1 = new AuthenticationStats(USER_ID_1, 228 300 /* totalAttempts */, 10 /* rejectedAttempts */, 229 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 230 AuthenticationStats authenticationStats2 = new AuthenticationStats(USER_ID_2, 231 100 /* totalAttempts */, 5 /* rejectedAttempts */, 232 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT); 233 234 // Sets up the shared preference with user 1 only. 235 when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( 236 Set.of(buildFrrStats(authenticationStats1))); 237 238 // Add data for user 2. 239 mAuthenticationStatsPersister.persistFrrStats(authenticationStats2.getUserId(), 240 authenticationStats2.getTotalAttempts(), 241 authenticationStats2.getRejectedAttempts(), 242 authenticationStats2.getEnrollmentNotifications(), 243 authenticationStats2.getModality()); 244 245 verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); 246 assertThat(mStringSetArgumentCaptor.getValue()) 247 .contains(buildFrrStats(authenticationStats2)); 248 } 249 250 @Test removeFrrStats_existingUser_shouldUpdateRecord()251 public void removeFrrStats_existingUser_shouldUpdateRecord() throws JSONException { 252 AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1, 253 300 /* totalAttempts */, 10 /* rejectedAttempts */, 254 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE); 255 when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn( 256 Set.of(buildFrrStats(authenticationStats))); 257 258 mAuthenticationStatsPersister.removeFrrStats(USER_ID_1); 259 260 verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture()); 261 assertThat(mStringSetArgumentCaptor.getValue()).doesNotContain(authenticationStats); 262 } 263 264 @Test persistFrrThreshold_shouldUpdateRecord()265 public void persistFrrThreshold_shouldUpdateRecord() { 266 mAuthenticationStatsPersister.persistFrrThreshold(FRR_THRESHOLD); 267 268 verify(mEditor).putFloat(eq(THRESHOLD_KEY), mFrrThresholdArgumentCaptor.capture()); 269 assertThat(mFrrThresholdArgumentCaptor.getValue()).isWithin(0f).of(FRR_THRESHOLD); 270 } 271 buildFrrStats(AuthenticationStats authenticationStats)272 private String buildFrrStats(AuthenticationStats authenticationStats) 273 throws JSONException { 274 if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FACE) { 275 return new JSONObject() 276 .put(USER_ID, authenticationStats.getUserId()) 277 .put(FACE_ATTEMPTS, authenticationStats.getTotalAttempts()) 278 .put(FACE_REJECTIONS, authenticationStats.getRejectedAttempts()) 279 .put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications()) 280 .toString(); 281 } else if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FINGERPRINT) { 282 return new JSONObject() 283 .put(USER_ID, authenticationStats.getUserId()) 284 .put(FINGERPRINT_ATTEMPTS, authenticationStats.getTotalAttempts()) 285 .put(FINGERPRINT_REJECTIONS, authenticationStats.getRejectedAttempts()) 286 .put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications()) 287 .toString(); 288 } 289 return ""; 290 } 291 } 292