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.DELTA_RESPONSES_CUT;
20 import static com.android.server.credentials.MetricUtilities.generateMetricKey;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.service.credentials.BeginCreateCredentialResponse;
25 import android.service.credentials.BeginGetCredentialResponse;
26 import android.service.credentials.CredentialEntry;
27 import android.util.Slog;
28 
29 import com.android.server.credentials.MetricUtilities;
30 import com.android.server.credentials.metrics.shared.ResponseCollective;
31 
32 import java.util.ArrayList;
33 import java.util.LinkedHashMap;
34 import java.util.List;
35 import java.util.Map;
36 
37 /**
38  * Provides contextual metric collection for objects generated from
39  * {@link com.android.server.credentials.ProviderSession} flows to isolate metric
40  * collection from the core codebase. For any future additions to the ProviderSession subclass
41  * list, metric collection should be added to this file.
42  */
43 public class ProviderSessionMetric {
44 
45     private static final String TAG = "ProviderSessionMetric";
46 
47     // Specific candidate provider metric for the provider this session handles
48     @NonNull
49     protected final CandidatePhaseMetric mCandidatePhasePerProviderMetric;
50 
51     // IFF there was an authentication entry clicked, this stores all required information for
52     // that event. This is for the 'get' flow. Notice these flows may be repetitive.
53     // Thus each provider stores a list of authentication metrics. The time between emits
54     // of these metrics should exceed 10 ms (given human reaction time is ~ 100's of ms), so emits
55     // will never collide. However, for aggregation, this will store information accordingly.
56     @NonNull
57     protected final List<BrowsedAuthenticationMetric> mBrowsedAuthenticationMetric =
58             new ArrayList<>();
59 
ProviderSessionMetric(int sessionIdTrackTwo)60     public ProviderSessionMetric(int sessionIdTrackTwo) {
61         mCandidatePhasePerProviderMetric = new CandidatePhaseMetric(sessionIdTrackTwo);
62         mBrowsedAuthenticationMetric.add(new BrowsedAuthenticationMetric(sessionIdTrackTwo));
63     }
64 
65     /**
66      * Retrieve the candidate provider phase metric and the data it contains.
67      */
getCandidatePhasePerProviderMetric()68     public CandidatePhaseMetric getCandidatePhasePerProviderMetric() {
69         return mCandidatePhasePerProviderMetric;
70     }
71 
72     /**
73      * Retrieves the authentication clicked metric information.
74      */
getBrowsedAuthenticationMetric()75     public List<BrowsedAuthenticationMetric> getBrowsedAuthenticationMetric() {
76         return mBrowsedAuthenticationMetric;
77     }
78 
79     /**
80      * This collects for ProviderSessions, with respect to the candidate providers, whether
81      * an exception occurred in the candidate call.
82      *
83      * @param hasException indicates if the candidate provider associated with an exception
84      */
collectCandidateExceptionStatus(boolean hasException)85     public void collectCandidateExceptionStatus(boolean hasException) {
86         try {
87             mCandidatePhasePerProviderMetric.setHasException(hasException);
88         } catch (Exception e) {
89             Slog.i(TAG, "Error while setting candidate metric exception " + e);
90         }
91     }
92 
93     /**
94      * This collects for ProviderSessions, with respect to the authentication entry provider,
95      * if an exception occurred in the authentication entry click. It's expected that these
96      * collections always occur after at least 1 authentication metric has been collected
97      * for the provider associated with this metric encapsulation.
98      *
99      * @param hasException indicates if the candidate provider from an authentication entry
100      *                     associated with an exception
101      */
collectAuthenticationExceptionStatus(boolean hasException)102     public void collectAuthenticationExceptionStatus(boolean hasException) {
103         try {
104             BrowsedAuthenticationMetric mostRecentAuthenticationMetric =
105                     getUsedAuthenticationMetric();
106             mostRecentAuthenticationMetric.setHasException(hasException);
107         } catch (Exception e) {
108             Slog.i(TAG, "Error while setting authentication metric exception " + e);
109         }
110     }
111 
112     /**
113      * Collects the framework only exception encountered in a candidate flow.
114      * @param exceptionType the string, cut to desired length, of the exception type
115      */
collectCandidateFrameworkException(String exceptionType)116     public void collectCandidateFrameworkException(String exceptionType) {
117         try {
118             mCandidatePhasePerProviderMetric.setFrameworkException(exceptionType);
119         } catch (Exception e) {
120             Slog.i(TAG, "Unexpected error during candidate exception metric logging: " + e);
121         }
122     }
123 
collectAuthEntryUpdate(boolean isFailureStatus, boolean isCompletionStatus, int providerSessionUid)124     private void collectAuthEntryUpdate(boolean isFailureStatus,
125             boolean isCompletionStatus, int providerSessionUid) {
126         BrowsedAuthenticationMetric mostRecentAuthenticationMetric =
127                 getUsedAuthenticationMetric();
128         mostRecentAuthenticationMetric.setProviderUid(providerSessionUid);
129         if (isFailureStatus) {
130             mostRecentAuthenticationMetric.setAuthReturned(false);
131             mostRecentAuthenticationMetric.setProviderStatus(
132                     ProviderStatusForMetrics.QUERY_FAILURE
133                             .getMetricCode());
134         } else if (isCompletionStatus) {
135             mostRecentAuthenticationMetric.setAuthReturned(true);
136             mostRecentAuthenticationMetric.setProviderStatus(
137                     ProviderStatusForMetrics.QUERY_SUCCESS
138                             .getMetricCode());
139         }
140     }
141 
getUsedAuthenticationMetric()142     private BrowsedAuthenticationMetric getUsedAuthenticationMetric() {
143         return mBrowsedAuthenticationMetric
144                 .get(mBrowsedAuthenticationMetric.size() - 1);
145     }
146 
147     /**
148      * Used to collect metrics at the update stage when a candidate provider gives back an update.
149      *
150      * @param isFailureStatus indicates the candidate provider sent back a terminated response
151      * @param isCompletionStatus indicates the candidate provider sent back a completion response
152      * @param providerSessionUid the uid of the provider
153      * @param isPrimary indicates if this candidate provider was the primary provider
154      */
collectCandidateMetricUpdate(boolean isFailureStatus, boolean isCompletionStatus, int providerSessionUid, boolean isAuthEntry, boolean isPrimary)155     public void collectCandidateMetricUpdate(boolean isFailureStatus,
156             boolean isCompletionStatus, int providerSessionUid, boolean isAuthEntry,
157             boolean isPrimary) {
158         try {
159             if (isAuthEntry) {
160                 collectAuthEntryUpdate(isFailureStatus, isCompletionStatus, providerSessionUid);
161                 return;
162             }
163             mCandidatePhasePerProviderMetric.setPrimary(isPrimary);
164             mCandidatePhasePerProviderMetric.setCandidateUid(providerSessionUid);
165             mCandidatePhasePerProviderMetric
166                     .setQueryFinishTimeNanoseconds(System.nanoTime());
167             if (isFailureStatus) {
168                 mCandidatePhasePerProviderMetric.setQueryReturned(false);
169                 mCandidatePhasePerProviderMetric.setProviderQueryStatus(
170                         ProviderStatusForMetrics.QUERY_FAILURE
171                                 .getMetricCode());
172             } else if (isCompletionStatus) {
173                 mCandidatePhasePerProviderMetric.setQueryReturned(true);
174                 mCandidatePhasePerProviderMetric.setProviderQueryStatus(
175                         ProviderStatusForMetrics.QUERY_SUCCESS
176                                 .getMetricCode());
177             }
178         } catch (Exception e) {
179             Slog.i(TAG, "Unexpected error during candidate update metric logging: " + e);
180         }
181     }
182 
183     /**
184      * Starts the collection of a single provider metric in the candidate phase of the API flow.
185      * It's expected that this should be called at the start of the query phase so that session id
186      * and timestamps can be shared. They can be accessed granular-ly through the underlying
187      * objects, but for {@link com.android.server.credentials.ProviderSession} context metrics,
188      * it's recommended to use these context-specified methods.
189      *
190      * @param initMetric the pre candidate phase metric collection object of type
191      * {@link InitialPhaseMetric} used to transfer initial information
192      */
collectCandidateMetricSetupViaInitialMetric(InitialPhaseMetric initMetric)193     public void collectCandidateMetricSetupViaInitialMetric(InitialPhaseMetric initMetric) {
194         try {
195             mCandidatePhasePerProviderMetric.setServiceBeganTimeNanoseconds(
196                     initMetric.getCredentialServiceStartedTimeNanoseconds());
197             mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime());
198         } catch (Exception e) {
199             Slog.i(TAG, "Unexpected error during candidate setup metric logging: " + e);
200         }
201     }
202 
203     /**
204      * Once candidate providers give back entries, this helps collect their info for metric
205      * purposes.
206      *
207      * @param response contains entries and data from the candidate provider responses
208      * @param isAuthEntry indicates if this is an auth entry collection or not
209      * @param initialPhaseMetric for create flows, this helps identify the response type, which
210      *                           will identify the *type* of create flow, especially important in
211      *                           track 2. This is expected to be null in get flows.
212      * @param <R> the response type associated with the API flow in progress
213      */
collectCandidateEntryMetrics(R response, boolean isAuthEntry, @Nullable InitialPhaseMetric initialPhaseMetric)214     public <R> void collectCandidateEntryMetrics(R response, boolean isAuthEntry,
215             @Nullable InitialPhaseMetric initialPhaseMetric) {
216         try {
217             if (response instanceof BeginGetCredentialResponse) {
218                 beginGetCredentialResponseCollectionCandidateEntryMetrics(
219                         (BeginGetCredentialResponse) response, isAuthEntry);
220             } else if (response instanceof BeginCreateCredentialResponse) {
221                 beginCreateCredentialResponseCollectionCandidateEntryMetrics(
222                         (BeginCreateCredentialResponse) response, initialPhaseMetric);
223             } else {
224                 Slog.i(TAG, "Your response type is unsupported for candidate metric logging");
225             }
226         } catch (Exception e) {
227             Slog.i(TAG, "Unexpected error during candidate entry metric logging: " + e);
228         }
229     }
230 
231     /**
232      * Once entries are received from the registry, this helps collect their info for metric
233      * purposes.
234      *
235      * @param entries contains matching entries from the Credential Registry.
236      */
collectCandidateEntryMetrics(List<CredentialEntry> entries)237     public void collectCandidateEntryMetrics(List<CredentialEntry> entries) {
238         int numCredEntries = entries.size();
239         int numRemoteEntry = MetricUtilities.ZERO;
240         int numActionEntries = MetricUtilities.ZERO;
241         int numAuthEntries = MetricUtilities.ZERO;
242         Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>();
243         Map<String, Integer> responseCounts = new LinkedHashMap<>();
244         entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry);
245         entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCredEntries);
246         entryCounts.put(EntryEnum.ACTION_ENTRY, numActionEntries);
247         entryCounts.put(EntryEnum.AUTHENTICATION_ENTRY, numAuthEntries);
248 
249         entries.forEach(entry -> {
250             String entryKey = generateMetricKey(entry.getType(), DELTA_RESPONSES_CUT);
251             responseCounts.put(entryKey, responseCounts.getOrDefault(entryKey, 0) + 1);
252         });
253         ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts);
254         mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective);
255     }
256 
257     /**
258      * This sets up an authentication metric collector to the flow. This must be called before
259      * any logical edits are done in a new authentication entry metric collection.
260      */
createAuthenticationBrowsingMetric()261     public void createAuthenticationBrowsingMetric() {
262         BrowsedAuthenticationMetric browsedAuthenticationMetric =
263                 new BrowsedAuthenticationMetric(mCandidatePhasePerProviderMetric
264                         .getSessionIdProvider());
265         mBrowsedAuthenticationMetric.add(browsedAuthenticationMetric);
266     }
267 
beginCreateCredentialResponseCollectionCandidateEntryMetrics( BeginCreateCredentialResponse response, InitialPhaseMetric initialPhaseMetric)268     private void beginCreateCredentialResponseCollectionCandidateEntryMetrics(
269             BeginCreateCredentialResponse response, InitialPhaseMetric initialPhaseMetric) {
270         Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>();
271         var createEntries = response.getCreateEntries();
272         int numRemoteEntry = response.getRemoteCreateEntry() == null ? MetricUtilities.ZERO :
273                 MetricUtilities.UNIT;
274         int numCreateEntries = createEntries.size();
275         entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry);
276         entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCreateEntries);
277 
278         Map<String, Integer> responseCounts = new LinkedHashMap<>();
279         String[] requestStrings = initialPhaseMetric == null ? new String[0] :
280                 initialPhaseMetric.getUniqueRequestStrings();
281         if (requestStrings.length > 0) {
282             responseCounts.put(requestStrings[0], initialPhaseMetric.getUniqueRequestCounts()[0]);
283         }
284 
285         ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts);
286         mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective);
287     }
288 
beginGetCredentialResponseCollectionCandidateEntryMetrics( BeginGetCredentialResponse response, boolean isAuthEntry)289     private void beginGetCredentialResponseCollectionCandidateEntryMetrics(
290             BeginGetCredentialResponse response, boolean isAuthEntry) {
291         Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>();
292         Map<String, Integer> responseCounts = new LinkedHashMap<>();
293         int numCredEntries = response.getCredentialEntries().size();
294         int numActionEntries = response.getActions().size();
295         int numAuthEntries = response.getAuthenticationActions().size();
296         int numRemoteEntry = response.getRemoteCredentialEntry() != null ? MetricUtilities.ZERO :
297                 MetricUtilities.UNIT;
298         entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry);
299         entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCredEntries);
300         entryCounts.put(EntryEnum.ACTION_ENTRY, numActionEntries);
301         entryCounts.put(EntryEnum.AUTHENTICATION_ENTRY, numAuthEntries);
302 
303         response.getCredentialEntries().forEach(entry -> {
304             String entryKey = generateMetricKey(entry.getType(), DELTA_RESPONSES_CUT);
305             responseCounts.put(entryKey, responseCounts.getOrDefault(entryKey, 0) + 1);
306         });
307 
308         ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts);
309 
310         if (!isAuthEntry) {
311             mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective);
312         } else {
313             // The most recent auth entry must be created already
314             var browsedAuthenticationMetric =
315                     mBrowsedAuthenticationMetric.get(mBrowsedAuthenticationMetric.size() - 1);
316             browsedAuthenticationMetric.setAuthEntryCollective(responseCollective);
317         }
318     }
319 }
320