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.credentials.metrics; 18 19 import static com.android.server.credentials.MetricUtilities.DEFAULT_INT_32; 20 import static com.android.server.credentials.MetricUtilities.DELTA_EXCEPTION_CUT; 21 import static com.android.server.credentials.MetricUtilities.DELTA_RESPONSES_CUT; 22 import static com.android.server.credentials.MetricUtilities.generateMetricKey; 23 import static com.android.server.credentials.MetricUtilities.logApiCalledAggregateCandidate; 24 import static com.android.server.credentials.MetricUtilities.logApiCalledAuthenticationMetric; 25 import static com.android.server.credentials.MetricUtilities.logApiCalledCandidateGetMetric; 26 import static com.android.server.credentials.MetricUtilities.logApiCalledCandidatePhase; 27 import static com.android.server.credentials.MetricUtilities.logApiCalledFinalPhase; 28 import static com.android.server.credentials.MetricUtilities.logApiCalledNoUidFinal; 29 import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL; 30 import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY; 31 32 import android.annotation.NonNull; 33 import android.annotation.UserIdInt; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.credentials.CreateCredentialRequest; 37 import android.credentials.GetCredentialRequest; 38 import android.credentials.selection.IntentCreationResult; 39 import android.credentials.selection.UserSelectionDialogResult; 40 import android.util.Slog; 41 42 import com.android.server.credentials.MetricUtilities; 43 import com.android.server.credentials.ProviderSession; 44 45 import java.util.ArrayList; 46 import java.util.LinkedHashMap; 47 import java.util.List; 48 import java.util.Map; 49 50 /** 51 * Provides contextual metric collection for objects generated from classes such as 52 * {@link com.android.server.credentials.GetRequestSession}, 53 * {@link com.android.server.credentials.CreateRequestSession}, 54 * and {@link com.android.server.credentials.ClearRequestSession} flows to isolate metric 55 * collection from the core codebase. For any future additions to the RequestSession subclass 56 * list, metric collection should be added to this file. 57 */ 58 public class RequestSessionMetric { 59 private static final String TAG = "RequestSessionMetric"; 60 61 // As emits occur in sequential order, increment this counter and utilize 62 protected int mSequenceCounter = 0; 63 64 protected final InitialPhaseMetric mInitialPhaseMetric; 65 protected final ChosenProviderFinalPhaseMetric 66 mChosenProviderFinalPhaseMetric; 67 protected List<CandidateBrowsingPhaseMetric> mCandidateBrowsingPhaseMetric = new ArrayList<>(); 68 // Specific aggregate candidate provider metric for the provider this session handles 69 @NonNull 70 protected final CandidateAggregateMetric mCandidateAggregateMetric; 71 // Since track two is shared, this allows provider sessions to capture a metric-specific 72 // session token for the flow where the provider is known 73 private final int mSessionIdTrackTwo; 74 RequestSessionMetric(int sessionIdTrackOne, int sessionIdTrackTwo)75 public RequestSessionMetric(int sessionIdTrackOne, int sessionIdTrackTwo) { 76 mSessionIdTrackTwo = sessionIdTrackTwo; 77 mInitialPhaseMetric = new InitialPhaseMetric(sessionIdTrackOne); 78 mCandidateAggregateMetric = new CandidateAggregateMetric(sessionIdTrackOne); 79 mChosenProviderFinalPhaseMetric = new ChosenProviderFinalPhaseMetric( 80 sessionIdTrackOne, sessionIdTrackTwo); 81 } 82 83 /** 84 * Increments the metric emit sequence counter and returns the current state value of the 85 * sequence. 86 * 87 * @return the current state value of the metric emit sequence. 88 */ returnIncrementSequence()89 public int returnIncrementSequence() { 90 return ++mSequenceCounter; 91 } 92 93 94 /** 95 * @return the initial metrics associated with the request session 96 */ getInitialPhaseMetric()97 public InitialPhaseMetric getInitialPhaseMetric() { 98 return mInitialPhaseMetric; 99 } 100 101 /** 102 * @return the aggregate candidate phase metrics associated with the request session 103 */ getCandidateAggregateMetric()104 public CandidateAggregateMetric getCandidateAggregateMetric() { 105 return mCandidateAggregateMetric; 106 } 107 108 /** 109 * Upon starting the service, this fills the initial phase metric properly. 110 * 111 * @param timestampStarted the timestamp the service begins at 112 * @param mCallingUid the calling process's uid 113 * @param metricCode typically pulled from {@link ApiName} 114 */ collectInitialPhaseMetricInfo(long timestampStarted, int mCallingUid, int metricCode)115 public void collectInitialPhaseMetricInfo(long timestampStarted, 116 int mCallingUid, int metricCode) { 117 try { 118 mInitialPhaseMetric.setCredentialServiceStartedTimeNanoseconds(timestampStarted); 119 mInitialPhaseMetric.setCallerUid(mCallingUid); 120 mInitialPhaseMetric.setApiName(metricCode); 121 } catch (Exception e) { 122 Slog.i(TAG, "Unexpected error collecting initial phase metric start info: " + e); 123 } 124 } 125 126 /** 127 * Collects whether the UI returned for metric purposes. 128 * 129 * @param uiReturned indicates whether the ui returns or not 130 */ collectUiReturnedFinalPhase(boolean uiReturned)131 public void collectUiReturnedFinalPhase(boolean uiReturned) { 132 try { 133 mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned); 134 } catch (Exception e) { 135 Slog.i(TAG, "Unexpected error collecting ui end time metric: " + e); 136 } 137 } 138 139 /** 140 * Sets the start time for the UI being called for metric purposes. 141 * 142 * @param uiCallStartTime the nanosecond time when the UI call began 143 */ collectUiCallStartTime(long uiCallStartTime)144 public void collectUiCallStartTime(long uiCallStartTime) { 145 try { 146 mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(uiCallStartTime); 147 } catch (Exception e) { 148 Slog.i(TAG, "Unexpected error collecting ui start metric: " + e); 149 } 150 } 151 152 /** 153 * When the UI responds to the framework at the very final phase, this collects the timestamp 154 * and status of the return for metric purposes. 155 * 156 * @param uiReturned indicates whether the ui returns or not 157 * @param uiEndTimestamp the nanosecond time when the UI call ended 158 */ collectUiResponseData(boolean uiReturned, long uiEndTimestamp)159 public void collectUiResponseData(boolean uiReturned, long uiEndTimestamp) { 160 try { 161 mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned); 162 mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(uiEndTimestamp); 163 } catch (Exception e) { 164 Slog.i(TAG, "Unexpected error collecting ui response metric: " + e); 165 } 166 } 167 168 /** 169 * Collects the final chosen provider status, with the status value coming from 170 * {@link ApiStatus}. 171 * 172 * @param status the final status of the chosen provider 173 */ collectChosenProviderStatus(int status)174 public void collectChosenProviderStatus(int status) { 175 try { 176 mChosenProviderFinalPhaseMetric.setChosenProviderStatus(status); 177 } catch (Exception e) { 178 Slog.i(TAG, "Unexpected error setting chosen provider status metric: " + e); 179 } 180 } 181 182 /** 183 * Collects initializations for Create flow metrics. 184 * 185 * @param origin indicates if an origin was passed in or not 186 */ collectCreateFlowInitialMetricInfo(boolean origin, CreateCredentialRequest request)187 public void collectCreateFlowInitialMetricInfo(boolean origin, 188 CreateCredentialRequest request) { 189 try { 190 mInitialPhaseMetric.setOriginSpecified(origin); 191 mInitialPhaseMetric.setRequestCounts(Map.of(generateMetricKey(request.getType(), 192 DELTA_RESPONSES_CUT), MetricUtilities.UNIT)); 193 } catch (Exception e) { 194 Slog.i(TAG, "Unexpected error collecting create flow metric: " + e); 195 } 196 } 197 198 // Used by get flows to generate the unique request count maps getRequestCountMap(GetCredentialRequest request)199 private Map<String, Integer> getRequestCountMap(GetCredentialRequest request) { 200 Map<String, Integer> uniqueRequestCounts = new LinkedHashMap<>(); 201 try { 202 request.getCredentialOptions().forEach(option -> { 203 String optionKey = generateMetricKey(option.getType(), DELTA_RESPONSES_CUT); 204 uniqueRequestCounts.put(optionKey, uniqueRequestCounts.getOrDefault(optionKey, 205 0) + 1); 206 }); 207 } catch (Exception e) { 208 Slog.i(TAG, "Unexpected error during get request count map metric logging: " + e); 209 } 210 return uniqueRequestCounts; 211 } 212 213 /** 214 * Collects initializations for Get flow metrics. 215 * 216 * @param request the get credential request containing information to parse for metrics 217 */ collectGetFlowInitialMetricInfo(GetCredentialRequest request)218 public void collectGetFlowInitialMetricInfo(GetCredentialRequest request) { 219 try { 220 mInitialPhaseMetric.setOriginSpecified(request.getOrigin() != null); 221 mInitialPhaseMetric.setRequestCounts(getRequestCountMap(request)); 222 } catch (Exception e) { 223 Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e); 224 } 225 } 226 227 /** 228 * Collects initializations for Get flow metrics. 229 * 230 * @param request the get credential request containing information to parse for metrics 231 * @param isApiPrepared indicates this API flow utilized the 'prepare' flow 232 */ collectGetFlowInitialMetricInfo(GetCredentialRequest request, boolean isApiPrepared)233 public void collectGetFlowInitialMetricInfo(GetCredentialRequest request, 234 boolean isApiPrepared) { 235 try { 236 collectGetFlowInitialMetricInfo(request); 237 mInitialPhaseMetric.setApiUsedPrepareFlow(isApiPrepared); 238 } catch (Exception e) { 239 Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e); 240 } 241 } 242 243 /** 244 * During browsing, where multiple entries can be selected, this collects the browsing phase 245 * metric information. This is emitted together with the final phase, and the recursive path 246 * with authentication entries, which may occur in rare circumstances, are captured. 247 * 248 * @param selection contains the selected entry key type 249 * @param selectedProviderPhaseMetric contains the utility information of the selected provider 250 */ collectMetricPerBrowsingSelect(UserSelectionDialogResult selection, CandidatePhaseMetric selectedProviderPhaseMetric)251 public void collectMetricPerBrowsingSelect(UserSelectionDialogResult selection, 252 CandidatePhaseMetric selectedProviderPhaseMetric) { 253 try { 254 CandidateBrowsingPhaseMetric browsingPhaseMetric = new CandidateBrowsingPhaseMetric(); 255 browsingPhaseMetric.setEntryEnum( 256 EntryEnum.getMetricCodeFromString(selection.getEntryKey())); 257 browsingPhaseMetric.setProviderUid(selectedProviderPhaseMetric.getCandidateUid()); 258 mCandidateBrowsingPhaseMetric.add(browsingPhaseMetric); 259 } catch (Exception e) { 260 Slog.i(TAG, "Unexpected error collecting browsing metric: " + e); 261 } 262 } 263 264 /** 265 * This collects the final chosen class type. While it is possible to collect this during 266 * browsing, note this only collects the final tapped bit. 267 * 268 * @param createOrCredentialType the string type to collect when an entry is tapped by the user 269 */ collectChosenClassType(String createOrCredentialType)270 public void collectChosenClassType(String createOrCredentialType) { 271 String truncatedType = generateMetricKey(createOrCredentialType, DELTA_RESPONSES_CUT); 272 try { 273 mChosenProviderFinalPhaseMetric.setChosenClassType(truncatedType); 274 } catch (Exception e) { 275 Slog.i(TAG, "Unexpected error collecting chosen class type metadata: " + e); 276 } 277 } 278 279 /** 280 * Updates the final phase metric with the designated bit. 281 * 282 * @param exceptionBitFinalPhase represents if the final phase provider had an exception 283 */ setHasExceptionFinalPhase(boolean exceptionBitFinalPhase)284 public void setHasExceptionFinalPhase(boolean exceptionBitFinalPhase) { 285 try { 286 mChosenProviderFinalPhaseMetric.setHasException(exceptionBitFinalPhase); 287 } catch (Exception e) { 288 Slog.i(TAG, "Unexpected error setting final exception metric: " + e); 289 } 290 } 291 292 /** 293 * This allows collecting the framework exception string for the final phase metric. 294 * NOTE that this exception will be cut for space optimizations. 295 * 296 * @param exception the framework exception that is being recorded 297 */ collectFrameworkException(String exception)298 public void collectFrameworkException(String exception) { 299 try { 300 mChosenProviderFinalPhaseMetric.setFrameworkException( 301 generateMetricKey(exception, DELTA_EXCEPTION_CUT)); 302 } catch (Exception e) { 303 Slog.w(TAG, "Unexpected error during metric logging: " + e); 304 } 305 } 306 307 /** Log results of the device Credential Manager UI configuration. */ collectUiConfigurationResults(Context context, IntentCreationResult result, @UserIdInt int userId)308 public void collectUiConfigurationResults(Context context, IntentCreationResult result, 309 @UserIdInt int userId) { 310 try { 311 mChosenProviderFinalPhaseMetric.setOemUiUid(MetricUtilities.getPackageUid( 312 context, result.getOemUiPackageName(), userId)); 313 mChosenProviderFinalPhaseMetric.setFallbackUiUid(MetricUtilities.getPackageUid( 314 context, result.getFallbackUiPackageName(), userId)); 315 mChosenProviderFinalPhaseMetric.setOemUiUsageStatus( 316 OemUiUsageStatus.createFrom(result.getOemUiUsageStatus())); 317 } catch (Exception e) { 318 Slog.w(TAG, "Unexpected error during ui configuration result collection: " + e); 319 } 320 } 321 322 /** 323 * Allows encapsulating the overall final phase metric status from the chosen and final 324 * provider. 325 * 326 * @param hasException represents if the final phase provider had an exception 327 * @param finalStatus represents the final status of the chosen provider 328 */ collectFinalPhaseProviderMetricStatus(boolean hasException, ProviderStatusForMetrics finalStatus)329 public void collectFinalPhaseProviderMetricStatus(boolean hasException, 330 ProviderStatusForMetrics finalStatus) { 331 try { 332 mChosenProviderFinalPhaseMetric.setHasException(hasException); 333 mChosenProviderFinalPhaseMetric.setChosenProviderStatus( 334 finalStatus.getMetricCode()); 335 } catch (Exception e) { 336 Slog.i(TAG, "Unexpected error during final phase provider status metric logging: " + e); 337 } 338 } 339 340 /** 341 * Used to update metrics when a response is received in a RequestSession. 342 * 343 * @param componentName the component name associated with the provider the response is for 344 */ updateMetricsOnResponseReceived(Map<String, ProviderSession> providers, ComponentName componentName, boolean isPrimary)345 public void updateMetricsOnResponseReceived(Map<String, ProviderSession> providers, 346 ComponentName componentName, boolean isPrimary) { 347 try { 348 var chosenProviderSession = providers.get(componentName.flattenToString()); 349 if (chosenProviderSession != null) { 350 ProviderSessionMetric providerSessionMetric = 351 chosenProviderSession.getProviderSessionMetric(); 352 collectChosenMetricViaCandidateTransfer(providerSessionMetric 353 .getCandidatePhasePerProviderMetric(), isPrimary); 354 } 355 } catch (Exception e) { 356 Slog.i(TAG, "Exception upon candidate to chosen metric transfer: " + e); 357 } 358 } 359 360 /** 361 * Called by RequestSessions upon chosen metric determination. It's expected that most bits 362 * are transferred here. However, certain new information, such as the selected provider's final 363 * exception bit, the framework to ui and back latency, or the ui response bit are set at other 364 * locations. Other information, such browsing metrics, api_status, and the sequence id count 365 * are combined during the final emit moment with the actual and official 366 * {@link com.android.internal.util.FrameworkStatsLog} metric generation. 367 * 368 * @param candidatePhaseMetric the componentName to associate with a provider 369 * @param isPrimary indicates that this chosen provider is the primary provider (or not) 370 */ collectChosenMetricViaCandidateTransfer(CandidatePhaseMetric candidatePhaseMetric, boolean isPrimary)371 public void collectChosenMetricViaCandidateTransfer(CandidatePhaseMetric candidatePhaseMetric, 372 boolean isPrimary) { 373 try { 374 mChosenProviderFinalPhaseMetric.setChosenUid(candidatePhaseMetric.getCandidateUid()); 375 mChosenProviderFinalPhaseMetric.setPrimary(isPrimary); 376 377 mChosenProviderFinalPhaseMetric.setQueryPhaseLatencyMicroseconds( 378 candidatePhaseMetric.getQueryLatencyMicroseconds()); 379 380 mChosenProviderFinalPhaseMetric.setServiceBeganTimeNanoseconds( 381 candidatePhaseMetric.getServiceBeganTimeNanoseconds()); 382 mChosenProviderFinalPhaseMetric.setQueryStartTimeNanoseconds( 383 candidatePhaseMetric.getStartQueryTimeNanoseconds()); 384 mChosenProviderFinalPhaseMetric.setQueryEndTimeNanoseconds(candidatePhaseMetric 385 .getQueryFinishTimeNanoseconds()); 386 mChosenProviderFinalPhaseMetric.setResponseCollective( 387 candidatePhaseMetric.getResponseCollective()); 388 mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime()); 389 } catch (Exception e) { 390 Slog.i(TAG, "Unexpected error during metric candidate to final transfer: " + e); 391 } 392 } 393 394 /** 395 * In the final phase, this helps log use cases that were either pure failures or user 396 * canceled. It's expected that {@link #collectFinalPhaseProviderMetricStatus(boolean, 397 * ProviderStatusForMetrics) collectFinalPhaseProviderMetricStatus} is called prior to this. 398 * Otherwise, the logging will miss required bits. 399 * 400 * @param isUserCanceledError a boolean indicating if the error was due to user cancelling 401 */ logFailureOrUserCancel(boolean isUserCanceledError)402 public void logFailureOrUserCancel(boolean isUserCanceledError) { 403 try { 404 if (isUserCanceledError) { 405 setHasExceptionFinalPhase(/* has_exception */ false); 406 logApiCalledAtFinish( 407 /* apiStatus */ ApiStatus.USER_CANCELED.getMetricCode()); 408 } else { 409 logApiCalledAtFinish( 410 /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); 411 } 412 } catch (Exception e) { 413 Slog.i(TAG, "Unexpected error during final metric failure emit: " + e); 414 } 415 } 416 417 /** 418 * Handles candidate phase metric emit in the RequestSession context, after the candidate phase 419 * completes. 420 * 421 * @param providers a map with known providers and their held metric objects 422 */ logCandidatePhaseMetrics(Map<String, ProviderSession> providers)423 public void logCandidatePhaseMetrics(Map<String, ProviderSession> providers) { 424 try { 425 logApiCalledCandidatePhase(providers, ++mSequenceCounter, mInitialPhaseMetric); 426 if (mInitialPhaseMetric.getApiName() == GET_CREDENTIAL.getMetricCode() 427 || mInitialPhaseMetric.getApiName() == GET_CREDENTIAL_VIA_REGISTRY 428 .getMetricCode()) { 429 logApiCalledCandidateGetMetric(providers, mSequenceCounter); 430 } 431 } catch (Exception e) { 432 Slog.i(TAG, "Unexpected error during candidate metric emit: " + e); 433 } 434 } 435 436 /** 437 * Handles aggregate candidate phase metric emits in the RequestSession context, after the 438 * candidate phase completes. 439 * 440 * @param providers a map with known providers and their held metric objects 441 */ logCandidateAggregateMetrics(Map<String, ProviderSession> providers)442 public void logCandidateAggregateMetrics(Map<String, ProviderSession> providers) { 443 try { 444 mCandidateAggregateMetric.collectAverages(providers); 445 logApiCalledAggregateCandidate(mCandidateAggregateMetric, ++mSequenceCounter); 446 } catch (Exception e) { 447 Slog.i(TAG, "Unexpected error during aggregate candidate logging " + e); 448 } 449 } 450 451 /** 452 * This logs the authentication entry when browsed. Combined with the known browsed clicks 453 * in the {@link ChosenProviderFinalPhaseMetric}, this fully captures the authentication entry 454 * logic for multiple loops. An auth entry may have default or missing data, but if a provider 455 * was never assigned to an auth entry, this indicates an auth entry was never clicked. 456 * This case is handled in this emit. 457 * 458 * @param browsedAuthenticationMetric the authentication metric information to emit 459 */ logAuthEntry(BrowsedAuthenticationMetric browsedAuthenticationMetric)460 public void logAuthEntry(BrowsedAuthenticationMetric browsedAuthenticationMetric) { 461 try { 462 if (browsedAuthenticationMetric.getProviderUid() == DEFAULT_INT_32) { 463 Slog.v(TAG, "An authentication entry was not clicked"); 464 return; 465 } 466 logApiCalledAuthenticationMetric(browsedAuthenticationMetric, ++mSequenceCounter); 467 } catch (Exception e) { 468 Slog.i(TAG, "Unexpected error during auth entry metric emit: " + e); 469 } 470 471 } 472 473 /** 474 * Handles the final logging for RequestSession context for the final phase. 475 * 476 * @param apiStatus the final status of the api being called 477 */ logApiCalledAtFinish(int apiStatus)478 public void logApiCalledAtFinish(int apiStatus) { 479 try { 480 logApiCalledFinalPhase(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric, 481 apiStatus, 482 ++mSequenceCounter); 483 logApiCalledNoUidFinal(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric, 484 apiStatus, 485 ++mSequenceCounter); 486 } catch (Exception e) { 487 Slog.i(TAG, "Unexpected error during final metric emit: " + e); 488 } 489 } 490 getSessionIdTrackTwo()491 public int getSessionIdTrackTwo() { 492 return mSessionIdTrackTwo; 493 } 494 } 495