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