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