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.shared;
18 
19 import android.annotation.NonNull;
20 
21 import com.android.server.credentials.MetricUtilities;
22 import com.android.server.credentials.metrics.EntryEnum;
23 
24 import java.util.Collections;
25 import java.util.LinkedHashMap;
26 import java.util.Map;
27 
28 /**
29  * Some data is directly shared between the
30  * {@link com.android.server.credentials.metrics.CandidatePhaseMetric} and the
31  * {@link com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric}. This
32  * aims to create an abstraction that holds that information, to avoid duplication.
33  *
34  * This class should be immutable and threadsafe once generated.
35  */
36 public class ResponseCollective {
37     /*
38     Abstract Function (responseCounts, entryCounts) -> A 'ResponseCollective' containing information
39     about a chosen or candidate providers available responses, be they entries or credentials.
40 
41     RepInvariant: mResponseCounts and mEntryCounts are always initialized
42 
43     Threadsafe and Immutability: Once generated, the maps remain unchangeable. The object is
44     threadsafe and immutable, and safe from external changes. This is threadsafe because it is
45     immutable after creation and only allows reads, not writes.
46     */
47 
48     private static final String TAG = "ResponseCollective";
49 
50     // Stores the deduped credential response information, eg {"response":5} for this provider
51     private final Map<String, Integer> mResponseCounts;
52     // Stores the deduped entry information, eg {ENTRY_ENUM:5} for this provider
53     private final Map<EntryEnum, Integer> mEntryCounts;
54 
ResponseCollective(@onNull Map<String, Integer> responseCounts, @NonNull Map<EntryEnum, Integer> entryCounts)55     public ResponseCollective(@NonNull Map<String, Integer> responseCounts,
56             @NonNull Map<EntryEnum, Integer> entryCounts) {
57         mResponseCounts = responseCounts == null ? new LinkedHashMap<>() :
58                 new LinkedHashMap<>(responseCounts);
59         mEntryCounts = entryCounts == null ? new LinkedHashMap<>() :
60                 new LinkedHashMap<>(entryCounts);
61     }
62 
63     /**
64      * Returns the unique, deduped, response classtypes for logging associated with this provider.
65      *
66      * @return a string array for deduped classtypes
67      */
getUniqueResponseStrings()68     public String[] getUniqueResponseStrings() {
69         String[] result = new String[mResponseCounts.keySet().size()];
70         mResponseCounts.keySet().toArray(result);
71         return result;
72     }
73 
74     /**
75      * Returns an unmodifiable map of the entry counts, safe under the immutability of the
76      * class the original map is held within.
77      * @return an unmodifiable map of the entry : counts
78      */
getEntryCountsMap()79     public Map<EntryEnum, Integer> getEntryCountsMap() {
80         return Collections.unmodifiableMap(mEntryCounts);
81     }
82 
83     /**
84      * Returns an unmodifiable map of the response counts, safe under the immutability of the
85      * class the original map is held within.
86      * @return an unmodifiable map of the response : counts
87      */
getResponseCountsMap()88     public Map<String, Integer> getResponseCountsMap() {
89         return Collections.unmodifiableMap(mResponseCounts);
90     }
91 
92     /**
93      * Returns the unique, deduped, response classtype counts for logging associated with this
94      * provider.
95      * @return a string array for deduped classtype counts
96      */
getUniqueResponseCounts()97     public int[] getUniqueResponseCounts() {
98         return mResponseCounts.values().stream().mapToInt(Integer::intValue).toArray();
99     }
100 
101     /**
102      * Returns the unique, deduped, entry types for logging associated with this provider.
103      * @return an int array for deduped entries
104      */
getUniqueEntries()105     public int[] getUniqueEntries() {
106         return mEntryCounts.keySet().stream().mapToInt(Enum::ordinal).toArray();
107     }
108 
109     /**
110      * Returns the unique, deduped, entry classtype counts for logging associated with this
111      * provider.
112      * @return a string array for deduped classtype counts
113      */
getUniqueEntryCounts()114     public int[] getUniqueEntryCounts() {
115         return mEntryCounts.values().stream().mapToInt(Integer::intValue).toArray();
116     }
117 
118     /**
119      * Given a specific {@link EntryEnum}, this provides us with the count of that entry within
120      * this particular provider.
121      * @param e the entry enum with which we want to know the count of
122      * @return a count of this particular entry enum stored by this provider
123      */
getCountForEntry(EntryEnum e)124     public int getCountForEntry(EntryEnum e) {
125         return mEntryCounts.getOrDefault(e, MetricUtilities.ZERO);
126     }
127 
128     /**
129      * Indicates the total number of existing entries for this provider.
130      * @return a count of the total number of entries for this provider
131      */
getNumEntriesTotal()132     public int getNumEntriesTotal() {
133         return mEntryCounts.values().stream().mapToInt(Integer::intValue).sum();
134     }
135 
136     /**
137      * This combines the current collective with another collective, only if that other
138      * collective is indeed a differing one in memory.
139      * @param other the other response collective to combine with
140      * @return a combined {@link ResponseCollective} object
141      */
combineCollectives(ResponseCollective other)142     public ResponseCollective combineCollectives(ResponseCollective other) {
143         if (this == other) {
144             return this;
145         }
146 
147         Map<String, Integer> responseCounts = new LinkedHashMap<>(other.mResponseCounts);
148         for (String response : mResponseCounts.keySet()) {
149             responseCounts.merge(response, mResponseCounts.get(response), Integer::sum);
150         }
151 
152         Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(other.mEntryCounts);
153         for (EntryEnum entry : mEntryCounts.keySet()) {
154             entryCounts.merge(entry, mEntryCounts.get(entry), Integer::sum);
155         }
156 
157         return new ResponseCollective(responseCounts, entryCounts);
158     }
159 
160     /**
161      * Given two maps of type : counts, this combines the second into the first, to get an aggregate
162      * deduped type:count output.
163      * @param first the first map of some type to counts used as the base
164      * @param second the second map of some type to counts that mixies with the first
165      * @param <T> The type of the object we are mix-deduping - i.e. responses or entries.
166      * @return the first map updated with the second map's information for type:counts
167      */
combineTypeCountMaps(Map<T, Integer> first, Map<T, Integer> second)168     public static <T> Map<T, Integer> combineTypeCountMaps(Map<T, Integer> first,
169             Map<T, Integer> second) {
170         for (T response : second.keySet()) {
171             first.put(response, first.getOrDefault(response, 0) + second.get(response));
172         }
173         return first;
174     }
175 }
176