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 * During browsing, where multiple entries can be selected, this collects the browsing phase 229 * metric information. This is emitted together with the final phase, and the recursive path 230 * with authentication entries, which may occur in rare circumstances, are captured. 231 * 232 * @param selection contains the selected entry key type 233 * @param selectedProviderPhaseMetric contains the utility information of the selected provider 234 */ collectMetricPerBrowsingSelect(UserSelectionDialogResult selection, CandidatePhaseMetric selectedProviderPhaseMetric)235 public void collectMetricPerBrowsingSelect(UserSelectionDialogResult selection, 236 CandidatePhaseMetric selectedProviderPhaseMetric) { 237 try { 238 CandidateBrowsingPhaseMetric browsingPhaseMetric = new CandidateBrowsingPhaseMetric(); 239 browsingPhaseMetric.setEntryEnum( 240 EntryEnum.getMetricCodeFromString(selection.getEntryKey())); 241 browsingPhaseMetric.setProviderUid(selectedProviderPhaseMetric.getCandidateUid()); 242 mCandidateBrowsingPhaseMetric.add(browsingPhaseMetric); 243 } catch (Exception e) { 244 Slog.i(TAG, "Unexpected error collecting browsing metric: " + e); 245 } 246 } 247 248 /** 249 * Updates the final phase metric with the designated bit. 250 * 251 * @param exceptionBitFinalPhase represents if the final phase provider had an exception 252 */ setHasExceptionFinalPhase(boolean exceptionBitFinalPhase)253 public void setHasExceptionFinalPhase(boolean exceptionBitFinalPhase) { 254 try { 255 mChosenProviderFinalPhaseMetric.setHasException(exceptionBitFinalPhase); 256 } catch (Exception e) { 257 Slog.i(TAG, "Unexpected error setting final exception metric: " + e); 258 } 259 } 260 261 /** 262 * This allows collecting the framework exception string for the final phase metric. 263 * NOTE that this exception will be cut for space optimizations. 264 * 265 * @param exception the framework exception that is being recorded 266 */ collectFrameworkException(String exception)267 public void collectFrameworkException(String exception) { 268 try { 269 mChosenProviderFinalPhaseMetric.setFrameworkException( 270 generateMetricKey(exception, DELTA_EXCEPTION_CUT)); 271 } catch (Exception e) { 272 Slog.w(TAG, "Unexpected error during metric logging: " + e); 273 } 274 } 275 276 /** Log results of the device Credential Manager UI configuration. */ collectUiConfigurationResults(Context context, IntentCreationResult result, @UserIdInt int userId)277 public void collectUiConfigurationResults(Context context, IntentCreationResult result, 278 @UserIdInt int userId) { 279 try { 280 mChosenProviderFinalPhaseMetric.setOemUiUid(MetricUtilities.getPackageUid( 281 context, result.getOemUiPackageName(), userId)); 282 mChosenProviderFinalPhaseMetric.setFallbackUiUid(MetricUtilities.getPackageUid( 283 context, result.getFallbackUiPackageName(), userId)); 284 mChosenProviderFinalPhaseMetric.setOemUiUsageStatus( 285 OemUiUsageStatus.createFrom(result.getOemUiUsageStatus())); 286 } catch (Exception e) { 287 Slog.w(TAG, "Unexpected error during ui configuration result collection: " + e); 288 } 289 } 290 291 /** 292 * Allows encapsulating the overall final phase metric status from the chosen and final 293 * provider. 294 * 295 * @param hasException represents if the final phase provider had an exception 296 * @param finalStatus represents the final status of the chosen provider 297 */ collectFinalPhaseProviderMetricStatus(boolean hasException, ProviderStatusForMetrics finalStatus)298 public void collectFinalPhaseProviderMetricStatus(boolean hasException, 299 ProviderStatusForMetrics finalStatus) { 300 try { 301 mChosenProviderFinalPhaseMetric.setHasException(hasException); 302 mChosenProviderFinalPhaseMetric.setChosenProviderStatus( 303 finalStatus.getMetricCode()); 304 } catch (Exception e) { 305 Slog.i(TAG, "Unexpected error during final phase provider status metric logging: " + e); 306 } 307 } 308 309 /** 310 * Used to update metrics when a response is received in a RequestSession. 311 * 312 * @param componentName the component name associated with the provider the response is for 313 */ updateMetricsOnResponseReceived(Map<String, ProviderSession> providers, ComponentName componentName, boolean isPrimary)314 public void updateMetricsOnResponseReceived(Map<String, ProviderSession> providers, 315 ComponentName componentName, boolean isPrimary) { 316 try { 317 var chosenProviderSession = providers.get(componentName.flattenToString()); 318 if (chosenProviderSession != null) { 319 ProviderSessionMetric providerSessionMetric = 320 chosenProviderSession.getProviderSessionMetric(); 321 collectChosenMetricViaCandidateTransfer(providerSessionMetric 322 .getCandidatePhasePerProviderMetric(), isPrimary); 323 } 324 } catch (Exception e) { 325 Slog.i(TAG, "Exception upon candidate to chosen metric transfer: " + e); 326 } 327 } 328 329 /** 330 * Called by RequestSessions upon chosen metric determination. It's expected that most bits 331 * are transferred here. However, certain new information, such as the selected provider's final 332 * exception bit, the framework to ui and back latency, or the ui response bit are set at other 333 * locations. Other information, such browsing metrics, api_status, and the sequence id count 334 * are combined during the final emit moment with the actual and official 335 * {@link com.android.internal.util.FrameworkStatsLog} metric generation. 336 * 337 * @param candidatePhaseMetric the componentName to associate with a provider 338 * @param isPrimary indicates that this chosen provider is the primary provider (or not) 339 */ collectChosenMetricViaCandidateTransfer(CandidatePhaseMetric candidatePhaseMetric, boolean isPrimary)340 public void collectChosenMetricViaCandidateTransfer(CandidatePhaseMetric candidatePhaseMetric, 341 boolean isPrimary) { 342 try { 343 mChosenProviderFinalPhaseMetric.setChosenUid(candidatePhaseMetric.getCandidateUid()); 344 mChosenProviderFinalPhaseMetric.setPrimary(isPrimary); 345 346 mChosenProviderFinalPhaseMetric.setQueryPhaseLatencyMicroseconds( 347 candidatePhaseMetric.getQueryLatencyMicroseconds()); 348 349 mChosenProviderFinalPhaseMetric.setServiceBeganTimeNanoseconds( 350 candidatePhaseMetric.getServiceBeganTimeNanoseconds()); 351 mChosenProviderFinalPhaseMetric.setQueryStartTimeNanoseconds( 352 candidatePhaseMetric.getStartQueryTimeNanoseconds()); 353 mChosenProviderFinalPhaseMetric.setQueryEndTimeNanoseconds(candidatePhaseMetric 354 .getQueryFinishTimeNanoseconds()); 355 mChosenProviderFinalPhaseMetric.setResponseCollective( 356 candidatePhaseMetric.getResponseCollective()); 357 mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime()); 358 } catch (Exception e) { 359 Slog.i(TAG, "Unexpected error during metric candidate to final transfer: " + e); 360 } 361 } 362 363 /** 364 * In the final phase, this helps log use cases that were either pure failures or user 365 * canceled. It's expected that {@link #collectFinalPhaseProviderMetricStatus(boolean, 366 * ProviderStatusForMetrics) collectFinalPhaseProviderMetricStatus} is called prior to this. 367 * Otherwise, the logging will miss required bits. 368 * 369 * @param isUserCanceledError a boolean indicating if the error was due to user cancelling 370 */ logFailureOrUserCancel(boolean isUserCanceledError)371 public void logFailureOrUserCancel(boolean isUserCanceledError) { 372 try { 373 if (isUserCanceledError) { 374 setHasExceptionFinalPhase(/* has_exception */ false); 375 logApiCalledAtFinish( 376 /* apiStatus */ ApiStatus.USER_CANCELED.getMetricCode()); 377 } else { 378 logApiCalledAtFinish( 379 /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); 380 } 381 } catch (Exception e) { 382 Slog.i(TAG, "Unexpected error during final metric failure emit: " + e); 383 } 384 } 385 386 /** 387 * Handles candidate phase metric emit in the RequestSession context, after the candidate phase 388 * completes. 389 * 390 * @param providers a map with known providers and their held metric objects 391 */ logCandidatePhaseMetrics(Map<String, ProviderSession> providers)392 public void logCandidatePhaseMetrics(Map<String, ProviderSession> providers) { 393 try { 394 logApiCalledCandidatePhase(providers, ++mSequenceCounter, mInitialPhaseMetric); 395 if (mInitialPhaseMetric.getApiName() == GET_CREDENTIAL.getMetricCode() 396 || mInitialPhaseMetric.getApiName() == GET_CREDENTIAL_VIA_REGISTRY 397 .getMetricCode()) { 398 logApiCalledCandidateGetMetric(providers, mSequenceCounter); 399 } 400 } catch (Exception e) { 401 Slog.i(TAG, "Unexpected error during candidate metric emit: " + e); 402 } 403 } 404 405 /** 406 * Handles aggregate candidate phase metric emits in the RequestSession context, after the 407 * candidate phase completes. 408 * 409 * @param providers a map with known providers and their held metric objects 410 */ logCandidateAggregateMetrics(Map<String, ProviderSession> providers)411 public void logCandidateAggregateMetrics(Map<String, ProviderSession> providers) { 412 try { 413 mCandidateAggregateMetric.collectAverages(providers); 414 logApiCalledAggregateCandidate(mCandidateAggregateMetric, ++mSequenceCounter); 415 } catch (Exception e) { 416 Slog.i(TAG, "Unexpected error during aggregate candidate logging " + e); 417 } 418 } 419 420 /** 421 * This logs the authentication entry when browsed. Combined with the known browsed clicks 422 * in the {@link ChosenProviderFinalPhaseMetric}, this fully captures the authentication entry 423 * logic for multiple loops. An auth entry may have default or missing data, but if a provider 424 * was never assigned to an auth entry, this indicates an auth entry was never clicked. 425 * This case is handled in this emit. 426 * 427 * @param browsedAuthenticationMetric the authentication metric information to emit 428 */ logAuthEntry(BrowsedAuthenticationMetric browsedAuthenticationMetric)429 public void logAuthEntry(BrowsedAuthenticationMetric browsedAuthenticationMetric) { 430 try { 431 if (browsedAuthenticationMetric.getProviderUid() == DEFAULT_INT_32) { 432 Slog.v(TAG, "An authentication entry was not clicked"); 433 return; 434 } 435 logApiCalledAuthenticationMetric(browsedAuthenticationMetric, ++mSequenceCounter); 436 } catch (Exception e) { 437 Slog.i(TAG, "Unexpected error during auth entry metric emit: " + e); 438 } 439 440 } 441 442 /** 443 * Handles the final logging for RequestSession context for the final phase. 444 * 445 * @param apiStatus the final status of the api being called 446 */ logApiCalledAtFinish(int apiStatus)447 public void logApiCalledAtFinish(int apiStatus) { 448 try { 449 logApiCalledFinalPhase(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric, 450 apiStatus, 451 ++mSequenceCounter); 452 logApiCalledNoUidFinal(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric, 453 apiStatus, 454 ++mSequenceCounter); 455 } catch (Exception e) { 456 Slog.i(TAG, "Unexpected error during final metric emit: " + e); 457 } 458 } 459 getSessionIdTrackTwo()460 public int getSessionIdTrackTwo() { 461 return mSessionIdTrackTwo; 462 } 463 } 464