• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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