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