1 /*
2  * Copyright (C) 2021 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.ims.rcs.uce.presence.publish;
18 
19 import android.telephony.CarrierConfigManager;
20 import android.util.ArrayMap;
21 import android.util.ArraySet;
22 import android.util.IndentingPrintWriter;
23 import android.util.Log;
24 
25 import com.android.ims.rcs.uce.util.FeatureTags;
26 
27 import java.io.PrintWriter;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.Map;
31 import java.util.Objects;
32 import java.util.Set;
33 import java.util.stream.Collectors;
34 
35 /**
36  * Parses the Android Carrier Configuration for service-description -> feature tag mappings and
37  * tracks the IMS registration to pass in the
38  * to determine capabilities for features that the framework does not manage.
39  *
40  * @see CarrierConfigManager.Ims#KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY for
41  * more information on the format of this key.
42  */
43 public class PublishServiceDescTracker {
44     private static final String TAG = "PublishServiceDescTracker";
45 
46     /**
47      * Map from (service-id, version) to the feature tags required in registration required in order
48      * for the RCS feature to be considered "capable".
49      * <p>
50      * See {@link
51      * CarrierConfigManager.Ims#KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY}
52      * for more information on how this can be overridden/extended.
53      */
54     private static final Map<ServiceDescription, Set<String>> DEFAULT_SERVICE_DESCRIPTION_MAP;
55     static {
56         ArrayMap<ServiceDescription, Set<String>> map = new ArrayMap<>(19);
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_IM, Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_IM))57         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_IM,
58                 Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_IM));
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_SESSION, Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_SESSION))59         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_SESSION,
60                 Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_SESSION));
map.put(ServiceDescription.SERVICE_DESCRIPTION_FT, Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER))61         map.put(ServiceDescription.SERVICE_DESCRIPTION_FT,
62                 Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER));
map.put(ServiceDescription.SERVICE_DESCRIPTION_FT_SMS, Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER_VIA_SMS))63         map.put(ServiceDescription.SERVICE_DESCRIPTION_FT_SMS,
64                 Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER_VIA_SMS));
map.put(ServiceDescription.SERVICE_DESCRIPTION_PRESENCE, Collections.singleton(FeatureTags.FEATURE_TAG_PRESENCE))65         map.put(ServiceDescription.SERVICE_DESCRIPTION_PRESENCE,
66                 Collections.singleton(FeatureTags.FEATURE_TAG_PRESENCE));
67         // Same service-ID & version for MMTEL, but different description.
map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE, Collections.singleton(FeatureTags.FEATURE_TAG_MMTEL))68         map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE,
69                 Collections.singleton(FeatureTags.FEATURE_TAG_MMTEL));
map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_MMTEL, FeatureTags.FEATURE_TAG_VIDEO)))70         map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO, new ArraySet<>(
71                 Arrays.asList(FeatureTags.FEATURE_TAG_MMTEL, FeatureTags.FEATURE_TAG_VIDEO)));
map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH, Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH))72         map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH,
73                 Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH));
map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH_SMS, Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH_VIA_SMS))74         map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH_SMS,
75                 Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH_VIA_SMS));
map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER, Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING))76         map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER,
77                 Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING));
map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL, Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY))78         map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL,
79                 Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY));
map.put(ServiceDescription.SERVICE_DESCRIPTION_POST_CALL, Collections.singleton(FeatureTags.FEATURE_TAG_POST_CALL))80         map.put(ServiceDescription.SERVICE_DESCRIPTION_POST_CALL,
81                 Collections.singleton(FeatureTags.FEATURE_TAG_POST_CALL));
map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_MAP, Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_MAP))82         map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_MAP,
83                 Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_MAP));
map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_SKETCH, Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_SKETCH))84         map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_SKETCH,
85                 Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_SKETCH));
86         // Feature tags defined twice for chatbot session because we want v1 and v2 based on bot
87         // version
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION, FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)))88         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION, new ArraySet<>(
89                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
90                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION, FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)))91         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2, new ArraySet<>(
92                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
93                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
94         // Feature tags defined twice for chatbot sa session because we want v1 and v2 based on bot
95         // version
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG, FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)))96         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION, new ArraySet<>(
97                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
98                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V2, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG, FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)))99         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V2, new ArraySet<>(
100                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
101                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_ROLE, Collections.singleton(FeatureTags.FEATURE_TAG_CHATBOT_ROLE))102         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_ROLE,
103                 Collections.singleton(FeatureTags.FEATURE_TAG_CHATBOT_ROLE));
104         DEFAULT_SERVICE_DESCRIPTION_MAP = Collections.unmodifiableMap(map);
105     }
106 
107     // Maps from ServiceDescription to the set of feature tags required to consider the feature
108     // capable for PUBLISH.
109     private final Map<ServiceDescription, Set<String>> mServiceDescriptionFeatureTagMap;
110     // Handles cases where multiple ServiceDescriptions match a subset of the same feature tags.
111     // This will be used to only include the feature tags where the
112     private final Set<ServiceDescription> mServiceDescriptionPartialMatches = new ArraySet<>();
113     // The capabilities calculated based off of the last IMS registration.
114     private final Set<ServiceDescription> mRegistrationCapabilities = new ArraySet<>();
115     // Contains the feature tags used in the last update to IMS registration.
116     private Set<String> mRegistrationFeatureTags = new ArraySet<>();
117 
118     /**
119      * Create a new instance, which incorporates any carrier config overrides of the default
120      * mapping.
121      */
fromCarrierConfig(String[] carrierConfig)122     public static PublishServiceDescTracker fromCarrierConfig(String[] carrierConfig) {
123         Map<ServiceDescription, Set<String>> elements = new ArrayMap<>();
124         for (Map.Entry<ServiceDescription, Set<String>> entry :
125                 DEFAULT_SERVICE_DESCRIPTION_MAP.entrySet()) {
126 
127             elements.put(entry.getKey(), entry.getValue().stream()
128                     .map(PublishServiceDescTracker::removeInconsistencies)
129                     .collect(Collectors.toSet()));
130         }
131         if (carrierConfig != null) {
132             for (String entry : carrierConfig) {
133                 String[] serviceDesc = entry.split("\\|");
134                 if (serviceDesc.length < 4) {
135                     Log.w(TAG, "fromCarrierConfig: error parsing " + entry);
136                     continue;
137                 }
138                 elements.put(new ServiceDescription(serviceDesc[0].trim(), serviceDesc[1].trim(),
139                         serviceDesc[2].trim()), parseFeatureTags(serviceDesc[3]));
140             }
141         }
142         return new PublishServiceDescTracker(elements);
143     }
144 
145     /**
146      * Parse the feature tags in the string, which will be separated by ";".
147      */
parseFeatureTags(String featureTags)148     private static Set<String> parseFeatureTags(String featureTags) {
149         // First, split feature tags into individual params
150         String[] featureTagSplit = featureTags.split(";");
151         if (featureTagSplit.length == 0) {
152             return Collections.emptySet();
153         }
154         ArraySet<String> tags = new ArraySet<>(featureTagSplit.length);
155         // Add each tag, first trying to remove inconsistencies in string matching that may cause
156         // it to fail.
157         for (String tag : featureTagSplit) {
158             tags.add(removeInconsistencies(tag));
159         }
160         return tags;
161     }
162 
PublishServiceDescTracker(Map<ServiceDescription, Set<String>> serviceFeatureTagMap)163     private PublishServiceDescTracker(Map<ServiceDescription, Set<String>> serviceFeatureTagMap) {
164         mServiceDescriptionFeatureTagMap = serviceFeatureTagMap;
165         Set<ServiceDescription> keySet = mServiceDescriptionFeatureTagMap.keySet();
166         // Go through and collect any ServiceDescriptions that have the same service-id & version
167         // (but not the same description) and add them to a "partial match" list.
168         for (ServiceDescription c : keySet) {
169             mServiceDescriptionPartialMatches.addAll(keySet.stream()
170                     .filter(s -> !Objects.equals(s, c) && isSimilar(c , s))
171                     .collect(Collectors.toList()));
172         }
173     }
174 
175     /**
176      * Update the IMS registration associated with this tracker.
177      * @param imsRegistration A List of feature tags that were associated with the last IMS
178      *                        registration.
179      */
updateImsRegistration(Set<String> imsRegistration)180     public void updateImsRegistration(Set<String> imsRegistration) {
181         Set<String> sanitizedTags = imsRegistration.stream()
182                 // Ensure formatting passed in is the same as format stored here.
183                 .map(PublishServiceDescTracker::parseFeatureTags)
184                 // Each entry should only contain one feature tag.
185                 .map(s -> s.iterator().next()).collect(Collectors.toSet());
186 
187         // For aliased service descriptions (service-id && version is the same, but desc is
188         // different), Keep a "score" of the number of feature tags that the service description
189         // has associated with it. If another is found with a higher score, replace this one.
190         Map<ServiceDescription, Integer> aliasedServiceDescScore = new ArrayMap<>();
191         synchronized (mRegistrationCapabilities) {
192             mRegistrationFeatureTags = imsRegistration;
193             mRegistrationCapabilities.clear();
194             for (Map.Entry<ServiceDescription, Set<String>> desc :
195                     mServiceDescriptionFeatureTagMap.entrySet()) {
196                 boolean found = true;
197                 for (String tag : desc.getValue()) {
198                     if (!sanitizedTags.contains(tag)) {
199                         found = false;
200                         break;
201                     }
202                 }
203                 if (found) {
204                     // There may be ambiguity with multiple entries having the same service-id &&
205                     // version, but not the same description. In this case, we need to find any
206                     // other entries with the same id & version and replace it with the new entry
207                     // if it matches more "completely", i.e. match "mmtel;video" over "mmtel" if the
208                     // registration set includes "mmtel;video". Skip putting that in for now and
209                     // instead track the match with the most feature tags associated with it that
210                     // are all found in the IMS registration.
211                     if (mServiceDescriptionPartialMatches.contains(desc.getKey())) {
212                         ServiceDescription aliasedDesc = aliasedServiceDescScore.keySet().stream()
213                                 .filter(s -> isSimilar(s, desc.getKey()))
214                                 .findFirst().orElse(null);
215                         if (aliasedDesc != null) {
216                             Integer prevEntrySize = aliasedServiceDescScore.get(aliasedDesc);
217                             if (prevEntrySize != null
218                                     // Overrides are added below the original map, so prefer those.
219                                     && (prevEntrySize <= desc.getValue().size())) {
220                                 aliasedServiceDescScore.remove(aliasedDesc);
221                                 aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
222                             }
223                         } else {
224                             aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
225                         }
226                     } else {
227                         mRegistrationCapabilities.add(desc.getKey());
228                     }
229                 }
230             }
231             // Collect the highest "scored" ServiceDescriptions and add themto registration caps.
232             mRegistrationCapabilities.addAll(aliasedServiceDescScore.keySet());
233         }
234     }
235 
236     /**
237      * @return A copy of the service-description pairs (service-id, version) that are associated
238      * with the last IMS registration update in {@link #updateImsRegistration(Set)}
239      */
copyRegistrationCapabilities()240     public Set<ServiceDescription> copyRegistrationCapabilities() {
241         synchronized (mRegistrationCapabilities) {
242             return new ArraySet<>(mRegistrationCapabilities);
243         }
244     }
245 
246     /**
247      * @return A copy of the last update to the IMS feature tags via {@link #updateImsRegistration}.
248      */
copyRegistrationFeatureTags()249     public Set<String> copyRegistrationFeatureTags() {
250         synchronized (mRegistrationCapabilities) {
251             return new ArraySet<>(mRegistrationFeatureTags);
252         }
253     }
254 
255     /**
256      * Dumps the current state of this tracker.
257      */
dump(PrintWriter printWriter)258     public void dump(PrintWriter printWriter) {
259         IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
260         pw.println("PublishServiceDescTracker");
261         pw.increaseIndent();
262 
263         pw.println("ServiceDescription -> Feature Tag Map:");
264         pw.increaseIndent();
265         for (Map.Entry<ServiceDescription, Set<String>> entry :
266                 mServiceDescriptionFeatureTagMap.entrySet()) {
267             pw.print(entry.getKey());
268             pw.print("->");
269             pw.println(entry.getValue());
270         }
271         pw.println();
272         pw.decreaseIndent();
273 
274         if (!mServiceDescriptionPartialMatches.isEmpty()) {
275             pw.println("Similar ServiceDescriptions:");
276             pw.increaseIndent();
277             for (ServiceDescription entry : mServiceDescriptionPartialMatches) {
278                 pw.println(entry);
279             }
280             pw.decreaseIndent();
281         } else {
282             pw.println("No Similar ServiceDescriptions:");
283         }
284         pw.println();
285 
286         pw.println("Last IMS registration update:");
287         pw.increaseIndent();
288         for (String entry : mRegistrationFeatureTags) {
289             pw.println(entry);
290         }
291         pw.println();
292         pw.decreaseIndent();
293 
294         pw.println("Capabilities:");
295         pw.increaseIndent();
296         for (ServiceDescription entry : mRegistrationCapabilities) {
297             pw.println(entry);
298         }
299         pw.println();
300         pw.decreaseIndent();
301 
302         pw.decreaseIndent();
303     }
304 
305     /**
306      * Test if two ServiceDescriptions are similar, meaning service-id && version are equal.
307      */
isSimilar(ServiceDescription a, ServiceDescription b)308     private static boolean isSimilar(ServiceDescription a, ServiceDescription b) {
309         return (a.serviceId.equals(b.serviceId) && a.version.equals(b.version));
310     }
311 
312     /**
313      * Remove any formatting inconsistencies that could make string matching difficult.
314      */
removeInconsistencies(String tag)315     private static String removeInconsistencies(String tag) {
316         tag = tag.toLowerCase();
317         tag = tag.replaceAll("\\s+", "");
318         return tag;
319     }
320 }
321