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<>(23);
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         // A map has one key and one value. And if the same key is used, the value is replaced
87         // with a new one.
88         // The service description between SERVICE_DESCRIPTION_CHATBOT_SESSION and
89         // SERVICE_DESCRIPTION_CHATBOT_SESSION_V1 is the same, but this is for botVersion=#1 .
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION, FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)))90         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION, new ArraySet<>(
91                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
92                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
93         // This is the service description for botVersion=#1,#2 .
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V1, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION, FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_SUPPORTED)))94         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V1, new ArraySet<>(
95                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
96                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_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_V2_SUPPORTED)))97         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2, new ArraySet<>(
98                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
99                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_SUPPORTED)));
100         // The service description between SERVICE_DESCRIPTION_CHATBOT_SA_SESSION and
101         // SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V1 is the same, but this is for botVersion=#1 .
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)))102         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION, new ArraySet<>(
103                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
104                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
105         // This is the service description for botVersion=#1,#2 .
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V1, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG, FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_SUPPORTED)))106         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V1, new ArraySet<>(
107                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
108                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_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_V2_SUPPORTED)))109         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V2, new ArraySet<>(
110                 Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
111                         FeatureTags.FEATURE_TAG_CHATBOT_VERSION_V2_SUPPORTED)));
map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_ROLE, Collections.singleton(FeatureTags.FEATURE_TAG_CHATBOT_ROLE))112         map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_ROLE,
113                 Collections.singleton(FeatureTags.FEATURE_TAG_CHATBOT_ROLE));
map.put(ServiceDescription.SERVICE_DESCRIPTION_SLM, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_PAGER_MODE, FeatureTags.FEATURE_TAG_LARGE_MODE, FeatureTags.FEATURE_TAG_DEFERRED_MESSAGING, FeatureTags.FEATURE_TAG_LARGE_PAGER_MODE)))114         map.put(ServiceDescription.SERVICE_DESCRIPTION_SLM, new ArraySet<>(
115                 Arrays.asList(FeatureTags.FEATURE_TAG_PAGER_MODE,
116                         FeatureTags.FEATURE_TAG_LARGE_MODE,
117                         FeatureTags.FEATURE_TAG_DEFERRED_MESSAGING,
118                         FeatureTags.FEATURE_TAG_LARGE_PAGER_MODE)));
map.put(ServiceDescription.SERVICE_DESCRIPTION_SLM_PAGER_LARGE, new ArraySet<>( Arrays.asList(FeatureTags.FEATURE_TAG_PAGER_MODE, FeatureTags.FEATURE_TAG_LARGE_MODE)))119         map.put(ServiceDescription.SERVICE_DESCRIPTION_SLM_PAGER_LARGE, new ArraySet<>(
120                 Arrays.asList(FeatureTags.FEATURE_TAG_PAGER_MODE,
121                         FeatureTags.FEATURE_TAG_LARGE_MODE)));
122         DEFAULT_SERVICE_DESCRIPTION_MAP = Collections.unmodifiableMap(map);
123     }
124 
125     // Maps from ServiceDescription to the set of feature tags required to consider the feature
126     // capable for PUBLISH.
127     private final Map<ServiceDescription, Set<String>> mServiceDescriptionFeatureTagMap;
128     // Handles cases where multiple ServiceDescriptions match a subset of the same feature tags.
129     // This will be used to only include the feature tags where the
130     private final Set<ServiceDescription> mServiceDescriptionPartialMatches = new ArraySet<>();
131     // The capabilities calculated based off of the last IMS registration.
132     private final Set<ServiceDescription> mRegistrationCapabilities = new ArraySet<>();
133     // Contains the feature tags used in the last update to IMS registration.
134     private Set<String> mRegistrationFeatureTags = new ArraySet<>();
135 
136     /**
137      * Create a new instance, which incorporates any carrier config overrides of the default
138      * mapping.
139      */
fromCarrierConfig(String[] carrierConfig)140     public static PublishServiceDescTracker fromCarrierConfig(String[] carrierConfig) {
141         Map<ServiceDescription, Set<String>> elements = new ArrayMap<>();
142         for (Map.Entry<ServiceDescription, Set<String>> entry :
143                 DEFAULT_SERVICE_DESCRIPTION_MAP.entrySet()) {
144 
145             elements.put(entry.getKey(), entry.getValue().stream()
146                     .map(PublishServiceDescTracker::removeInconsistencies)
147                     .collect(Collectors.toSet()));
148         }
149         if (carrierConfig != null) {
150             for (String entry : carrierConfig) {
151                 String[] serviceDesc = entry.split("\\|");
152                 if (serviceDesc.length < 4) {
153                     Log.w(TAG, "fromCarrierConfig: error parsing " + entry);
154                     continue;
155                 }
156                 elements.put(new ServiceDescription(serviceDesc[0].trim(), serviceDesc[1].trim(),
157                         serviceDesc[2].trim()), parseFeatureTags(serviceDesc[3]));
158             }
159         }
160         return new PublishServiceDescTracker(elements);
161     }
162 
163     /**
164      * Parse the feature tags in the string, which will be separated by ";".
165      */
parseFeatureTags(String featureTags)166     private static Set<String> parseFeatureTags(String featureTags) {
167         // First, split feature tags into individual params
168         String[] featureTagSplit = featureTags.split(";");
169         if (featureTagSplit.length == 0) {
170             return Collections.emptySet();
171         }
172         ArraySet<String> tags = new ArraySet<>(featureTagSplit.length);
173         // Add each tag, first trying to remove inconsistencies in string matching that may cause
174         // it to fail.
175         for (String tag : featureTagSplit) {
176             tags.add(removeInconsistencies(tag));
177         }
178         return tags;
179     }
180 
PublishServiceDescTracker(Map<ServiceDescription, Set<String>> serviceFeatureTagMap)181     private PublishServiceDescTracker(Map<ServiceDescription, Set<String>> serviceFeatureTagMap) {
182         mServiceDescriptionFeatureTagMap = serviceFeatureTagMap;
183         Set<ServiceDescription> keySet = mServiceDescriptionFeatureTagMap.keySet();
184         // Go through and collect any ServiceDescriptions that have the same service-id & version
185         // (but not the same description) and add them to a "partial match" list.
186         for (ServiceDescription c : keySet) {
187             mServiceDescriptionPartialMatches.addAll(keySet.stream()
188                     .filter(s -> !Objects.equals(s, c) && isSimilar(c , s))
189                     .collect(Collectors.toList()));
190         }
191     }
192 
193     /**
194      * Update the IMS registration associated with this tracker.
195      * @param imsRegistration A List of feature tags that were associated with the last IMS
196      *                        registration.
197      */
updateImsRegistration(Set<String> imsRegistration)198     public void updateImsRegistration(Set<String> imsRegistration) {
199         Set<String> sanitizedTags = imsRegistration.stream()
200                 // Ensure formatting passed in is the same as format stored here.
201                 .map(PublishServiceDescTracker::parseFeatureTags)
202                 // Each entry should only contain one feature tag.
203                 .map(s -> s.iterator().next()).collect(Collectors.toSet());
204         // For aliased service descriptions (service-id && version is the same, but desc is
205         // different), Keep a "score" of the number of feature tags that the service description
206         // has associated with it. If another is found with a higher score, replace this one.
207         Map<ServiceDescription, Integer> aliasedServiceDescScore = new ArrayMap<>();
208         synchronized (mRegistrationCapabilities) {
209             mRegistrationFeatureTags = imsRegistration;
210             mRegistrationCapabilities.clear();
211             for (Map.Entry<ServiceDescription, Set<String>> desc :
212                     mServiceDescriptionFeatureTagMap.entrySet()) {
213                 boolean found = true;
214                 for (String tag : desc.getValue()) {
215                     if (!sanitizedTags.contains(tag)) {
216                         found = false;
217                         break;
218                     }
219                 }
220                 if (found) {
221                     // There may be ambiguity with multiple entries having the same service-id &&
222                     // version, but not the same description. In this case, we need to find any
223                     // other entries with the same id & version and replace it with the new entry
224                     // if it matches more "completely", i.e. match "mmtel;video" over "mmtel" if the
225                     // registration set includes "mmtel;video". Skip putting that in for now and
226                     // instead track the match with the most feature tags associated with it that
227                     // are all found in the IMS registration.
228                     if (mServiceDescriptionPartialMatches.contains(desc.getKey())) {
229                         ServiceDescription aliasedDesc = aliasedServiceDescScore.keySet().stream()
230                                 .filter(s -> isSimilar(s, desc.getKey()))
231                                 .findFirst().orElse(null);
232                         if (aliasedDesc != null) {
233                             Integer prevEntrySize = aliasedServiceDescScore.get(aliasedDesc);
234                             if (prevEntrySize != null
235                                     // Overrides are added below the original map, so prefer those.
236                                     && (prevEntrySize <= desc.getValue().size())) {
237                                 aliasedServiceDescScore.remove(aliasedDesc);
238                                 aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
239                             }
240                         } else {
241                             aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
242                         }
243                     } else {
244                         mRegistrationCapabilities.add(desc.getKey());
245                     }
246                 }
247             }
248             // Collect the highest "scored" ServiceDescriptions and add themto registration caps.
249             mRegistrationCapabilities.addAll(aliasedServiceDescScore.keySet());
250         }
251     }
252 
253     /**
254      * @return A copy of the service-description pairs (service-id, version) that are associated
255      * with the last IMS registration update in {@link #updateImsRegistration(Set)}
256      */
copyRegistrationCapabilities()257     public Set<ServiceDescription> copyRegistrationCapabilities() {
258         synchronized (mRegistrationCapabilities) {
259             return new ArraySet<>(mRegistrationCapabilities);
260         }
261     }
262 
263     /**
264      * @return A copy of the last update to the IMS feature tags via {@link #updateImsRegistration}.
265      */
copyRegistrationFeatureTags()266     public Set<String> copyRegistrationFeatureTags() {
267         synchronized (mRegistrationCapabilities) {
268             return new ArraySet<>(mRegistrationFeatureTags);
269         }
270     }
271 
272     /**
273      * Dumps the current state of this tracker.
274      */
dump(PrintWriter printWriter)275     public void dump(PrintWriter printWriter) {
276         IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
277         pw.println("PublishServiceDescTracker");
278         pw.increaseIndent();
279 
280         pw.println("ServiceDescription -> Feature Tag Map:");
281         pw.increaseIndent();
282         for (Map.Entry<ServiceDescription, Set<String>> entry :
283                 mServiceDescriptionFeatureTagMap.entrySet()) {
284             pw.print(entry.getKey());
285             pw.print("->");
286             pw.println(entry.getValue());
287         }
288         pw.println();
289         pw.decreaseIndent();
290 
291         if (!mServiceDescriptionPartialMatches.isEmpty()) {
292             pw.println("Similar ServiceDescriptions:");
293             pw.increaseIndent();
294             for (ServiceDescription entry : mServiceDescriptionPartialMatches) {
295                 pw.println(entry);
296             }
297             pw.decreaseIndent();
298         } else {
299             pw.println("No Similar ServiceDescriptions:");
300         }
301         pw.println();
302 
303         pw.println("Last IMS registration update:");
304         pw.increaseIndent();
305         for (String entry : mRegistrationFeatureTags) {
306             pw.println(entry);
307         }
308         pw.println();
309         pw.decreaseIndent();
310 
311         pw.println("Capabilities:");
312         pw.increaseIndent();
313         for (ServiceDescription entry : mRegistrationCapabilities) {
314             pw.println(entry);
315         }
316         pw.println();
317         pw.decreaseIndent();
318 
319         pw.decreaseIndent();
320     }
321 
322     /**
323      * Test if two ServiceDescriptions are similar, meaning service-id && version are equal.
324      */
isSimilar(ServiceDescription a, ServiceDescription b)325     private static boolean isSimilar(ServiceDescription a, ServiceDescription b) {
326         return (a.serviceId.equals(b.serviceId) && a.version.equals(b.version));
327     }
328 
329     /**
330      * Remove any formatting inconsistencies that could make string matching difficult.
331      */
removeInconsistencies(String tag)332     private static String removeInconsistencies(String tag) {
333         tag = tag.toLowerCase();
334         tag = tag.replaceAll("\\s+", "");
335         return tag;
336     }
337 }
338