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