1 /* 2 * Copyright (C) 2020 Google LLC 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 package com.google.carrier; 17 18 import static com.google.common.collect.Multimaps.flatteningToMultimap; 19 import static com.google.common.collect.Multimaps.toMultimap; 20 import static java.nio.charset.StandardCharsets.UTF_8; 21 import static java.util.Comparator.comparing; 22 23 import com.beust.jcommander.JCommander; 24 import com.beust.jcommander.Parameter; 25 import com.beust.jcommander.Parameters; 26 import com.google.auto.value.AutoValue; 27 import com.google.common.base.Ascii; 28 import com.google.common.base.CharMatcher; 29 import com.google.common.collect.ImmutableList; 30 import com.google.common.collect.ImmutableMap; 31 import com.google.common.collect.ImmutableSet; 32 import com.google.common.collect.Multimap; 33 import com.google.common.collect.MultimapBuilder; 34 import com.google.protobuf.Descriptors; 35 import com.google.protobuf.TextFormat; 36 import com.google.carrier.CarrierConfig; 37 import com.google.carrier.CarrierId; 38 import com.google.carrier.CarrierList; 39 import com.google.carrier.CarrierMap; 40 import com.google.carrier.CarrierSettings; 41 import com.google.carrier.IntArray; 42 import com.google.carrier.MultiCarrierSettings; 43 import com.google.carrier.TextArray; 44 import com.android.providers.telephony.CarrierIdProto.CarrierAttribute; 45 import java.io.BufferedReader; 46 import java.io.BufferedWriter; 47 import java.io.File; 48 import java.io.FileInputStream; 49 import java.io.FileOutputStream; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.io.InputStreamReader; 53 import java.io.OutputStream; 54 import java.io.OutputStreamWriter; 55 import java.util.ArrayList; 56 import java.util.HashMap; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.TreeMap; 60 import java.util.regex.Matcher; 61 import java.util.regex.Pattern; 62 import javax.xml.parsers.DocumentBuilder; 63 import javax.xml.parsers.DocumentBuilderFactory; 64 import javax.xml.parsers.ParserConfigurationException; 65 import org.w3c.dom.Document; 66 import org.w3c.dom.Element; 67 import org.w3c.dom.NamedNodeMap; 68 import org.w3c.dom.Node; 69 import org.w3c.dom.NodeList; 70 import org.xml.sax.SAXException; 71 72 /** 73 * This command converts carrier config XML into text protobuf. 74 * 75 * <ul> 76 * <li>input: the assets/ from AOSP CarrierConfig app 77 * <li>input: vendor.xml file(s) which override(s) assets 78 * <li>input: a tier-1 carrier list in text protobuf (in --output_dir) 79 * <li>input: the version number for output files 80 * <li>output: an other_carriers.textpb - a list of other (non-tier-1) carriers 81 * <li>output: an others.textpb containing carrier configs for non tier-1 carriers 82 * <li>output: a .textpb for every single tier-1 carrier 83 * </ul> 84 */ 85 @Parameters(separators = "=") 86 public final class CarrierConfigConverterV2 { 87 @Parameter(names = "--assets", description = "The source AOSP assets/ directory.") 88 private String assetsDirName = "/tmp/carrierconfig/assets"; 89 90 @Parameter( 91 names = "--vendor_xml", 92 description = 93 "The source vendor.xml file(s). If multiple files provided, the order decides config" 94 + " precedence, ie. configs in a file are overwritten by configs in files AFTER it.") 95 private List<String> vendorXmlFiles = ImmutableList.of("/tmp/carrierconfig/vendor.xml"); 96 97 @Parameter( 98 names = "--output_dir", 99 description = "The destination data directory, with tier1_carriers.textpb in it.") 100 private String outputDir = "/tmp/carrierconfig/out"; 101 102 @Parameter(names = "--version", description = "The version number for all output textpb.") 103 private long version = 1L; 104 105 // For configs in vendor.xml w/o mcc/mnc, they are the default config values for all carriers. 106 // In CarrierSettings, a special mcc/mnc "000000" is used to look up default config. 107 private static final String MCCMNC_FOR_DEFAULT_SETTINGS = "000000"; 108 109 // Resource file path to the AOSP carrier list file 110 private static final String RESOURCE_CARRIER_LIST = 111 "/assets/latest_carrier_id/carrier_list.textpb"; 112 113 // Constants used in parsing XMLs. 114 private static final String XML_SUFFIX = ".xml"; 115 private static final String CARRIER_CONFIG_MCCMNC_XML_PREFIX = "carrier_config_mccmnc_"; 116 private static final String CARRIER_CONFIG_CID_XML_PREFIX = "carrier_config_carrierid_"; 117 private static final String KEY_MCCMNC_PREFIX = "mccmnc_"; 118 private static final String KEY_CID_PREFIX = "cid_"; 119 private static final String TAG_CARRIER_CONFIG = "carrier_config"; 120 121 /** Entry point when invoked from command line. */ main(String[] args)122 public static void main(String[] args) throws IOException { 123 CarrierConfigConverterV2 converter = new CarrierConfigConverterV2(); 124 new JCommander(converter, args); 125 converter.convert(); 126 } 127 128 /** Entry point when invoked from other Java code, eg. the server side conversion tool. */ convert( String vendorXmlFile, String assetsDirName, String outputDir, long version)129 public static void convert( 130 String vendorXmlFile, String assetsDirName, String outputDir, long version) 131 throws IOException { 132 CarrierConfigConverterV2 converter = new CarrierConfigConverterV2(); 133 converter.vendorXmlFiles = ImmutableList.of(vendorXmlFile); 134 converter.assetsDirName = assetsDirName; 135 converter.outputDir = outputDir; 136 converter.version = version; 137 converter.convert(); 138 } 139 convert()140 private void convert() throws IOException { 141 String carriersTextpbFile = getPathAsString(outputDir, "tier1_carriers.textpb"); 142 String settingsTextpbDir = getPathAsString(outputDir, "setting"); 143 CarrierList tier1Carriers; 144 ArrayList<CarrierMap> otherCarriers = new ArrayList<>(); 145 ArrayList<String> outFiles = new ArrayList<>(); 146 HashMap<CarrierId, Map<String, CarrierConfig.Config>> rawConfigs = new HashMap<>(); 147 TreeMap<String, CarrierConfig> tier1Configs = new TreeMap<>(); 148 TreeMap<String, CarrierConfig> othersConfigs = new TreeMap<>(); 149 DocumentBuilder xmlDocBuilder = getDocumentBuilder(); 150 Multimap<Integer, CarrierId> aospCarrierList = loadAospCarrierList(); 151 Multimap<CarrierId, Integer> reverseAospCarrierList = reverseAospCarrierList(aospCarrierList); 152 153 /* 154 * High-level flow: 155 * 1. Parse all input XMLs into memory 156 * 2. Collect a list of interested carriers from input, represented by CarrierId. 157 * 2. For each CarrierId, build its carreir configs, following AOSP DefaultCarrierConfigService. 158 * 3. Merge CarrierId's as per tier1_carriers.textpb 159 */ 160 161 // 1. Parse all input XMLs into memory 162 Map<String, Document> assetsXmls = new HashMap<>(); 163 List<Document> vendorXmls = new ArrayList<>(); 164 // Parse assets/carrier_config_*.xml 165 for (File childFile : new File(assetsDirName).listFiles()) { 166 String childFileName = childFile.getName(); 167 String fullChildName = childFile.getCanonicalPath(); 168 if (childFileName.startsWith(CARRIER_CONFIG_MCCMNC_XML_PREFIX)) { 169 String mccMnc = 170 childFileName.substring( 171 CARRIER_CONFIG_MCCMNC_XML_PREFIX.length(), childFileName.indexOf(XML_SUFFIX)); 172 if (!mccMnc.matches("\\d{5,6}")) { 173 throw new IOException("Invalid mcc/mnc " + mccMnc + " found in " + childFileName); 174 } 175 try { 176 assetsXmls.put(KEY_MCCMNC_PREFIX + mccMnc, parseXmlDoc(fullChildName, xmlDocBuilder)); 177 } catch (SAXException | IOException e) { 178 throw new IOException("Failed to parse " + childFileName, e); 179 } 180 } else if (childFileName.startsWith(CARRIER_CONFIG_CID_XML_PREFIX)) { 181 String cidAndCarrierName = 182 childFileName.substring( 183 CARRIER_CONFIG_CID_XML_PREFIX.length(), childFileName.indexOf(XML_SUFFIX)); 184 int cid = -1; 185 try { 186 cid = Integer.parseInt(cidAndCarrierName.split("_", -1)[0]); 187 } catch (NumberFormatException e) { 188 throw new IOException("Invalid carrierid found in " + childFileName, e); 189 } 190 try { 191 assetsXmls.put(KEY_CID_PREFIX + cid, parseXmlDoc(fullChildName, xmlDocBuilder)); 192 } catch (SAXException | IOException e) { 193 throw new IOException("Failed to parse " + childFileName, e); 194 } 195 } 196 // ignore other malformatted files. 197 } 198 // Parse vendor.xml files 199 for (String vendorXmlFile : vendorXmlFiles) { 200 try { 201 vendorXmls.add(parseXmlDoc(vendorXmlFile, xmlDocBuilder)); 202 } catch (SAXException | IOException e) { 203 throw new IOException("Failed to parse " + vendorXmlFile, e); 204 } 205 } 206 207 // 2. Collect all carriers from input, represented by CarrierId. 208 List<CarrierId> carriers = new ArrayList<>(); 209 // Traverse <carrier_config /> labels in each file. 210 for (Map.Entry<String, Document> xml : assetsXmls.entrySet()) { 211 if (xml.getKey().startsWith(KEY_MCCMNC_PREFIX)) { 212 String mccMnc = xml.getKey().substring(KEY_MCCMNC_PREFIX.length()); 213 for (Element element : getElementsByTagName(xml.getValue(), TAG_CARRIER_CONFIG)) { 214 try { 215 CarrierId id = parseCarrierId(element).setMccMnc(mccMnc).build(); 216 carriers.add(id); 217 } catch (UnsupportedOperationException e) { 218 throw new IOException("Unsupported syntax in assets/ for " + mccMnc, e); 219 } 220 } 221 } else if (xml.getKey().startsWith(KEY_CID_PREFIX)) { 222 int cid = Integer.parseInt(xml.getKey().substring(KEY_CID_PREFIX.length())); 223 if (aospCarrierList.containsKey(cid)) { 224 carriers.addAll(aospCarrierList.get(cid)); 225 } else { 226 System.err.printf("Undefined cid %d in assets/. Ignore.\n", cid); 227 } 228 } 229 } 230 for (Document vendorXml : vendorXmls) { 231 for (Element element : getElementsByTagName(vendorXml, TAG_CARRIER_CONFIG)) { 232 // First, try to parse cid 233 if (element.hasAttribute("cid")) { 234 String cidAsString = element.getAttribute("cid"); 235 int cid = Integer.parseInt(cidAsString); 236 if (aospCarrierList.containsKey(cid)) { 237 carriers.addAll(aospCarrierList.get(cid)); 238 } else { 239 System.err.printf("Undefined cid %d in vendor.xml. Ignore.\n", cid); 240 } 241 } else { 242 // Then, try to parse CarrierId 243 CarrierId.Builder id = parseCarrierId(element); 244 // A valid mccmnc is 5- or 6-digit. But vendor.xml see special cases below: 245 // Case 1: a <carrier_config> element may have neither "mcc" nor "mnc". 246 // Such a tag provides a base config value for all carriers. CarrierSettings uses 247 // mcc/mnc 000/000 to identify that, and does the merge at run time instead of 248 // build time (which is now). 249 // Case 2: a <carrier_config> element may have just "mcc" and not "mnc" for 250 // country-wise config. Such a element doesn't make a carrier; but still keep it so 251 // can be used if a mccmnc appears in APNs later. 252 if (id.getMccMnc().isEmpty()) { 253 // special case 1 254 carriers.add(id.setMccMnc(MCCMNC_FOR_DEFAULT_SETTINGS).build()); 255 } else if (id.getMccMnc().length() == 3) { 256 // special case 2 257 carriers.add(id.build()); 258 } else if (id.getMccMnc().length() == 5 || id.getMccMnc().length() == 6) { 259 // Normal mcc+mnc 260 carriers.add(id.build()); 261 } else { 262 System.err.printf("Invalid mcc/mnc: %s. Ignore.\n", id.getMccMnc()); 263 } 264 } 265 } 266 } 267 268 // 3. For each CarrierId, build its carrier configs, following AOSP DefaultCarrierConfigService. 269 for (CarrierId carrier : carriers) { 270 Map<String, CarrierConfig.Config> config = ImmutableMap.of(); 271 272 CarrierIdentifier id = getCid(carrier, reverseAospCarrierList); 273 if (id.getCarrierId() != -1) { 274 HashMap<String, CarrierConfig.Config> configBySpecificCarrierId = 275 parseCarrierConfigFromXml( 276 assetsXmls.get(KEY_CID_PREFIX + id.getSpecificCarrierId()), id); 277 HashMap<String, CarrierConfig.Config> configByCarrierId = 278 parseCarrierConfigFromXml(assetsXmls.get(KEY_CID_PREFIX + id.getCarrierId()), id); 279 HashMap<String, CarrierConfig.Config> configByMccMncFallBackCarrierId = 280 parseCarrierConfigFromXml(assetsXmls.get(KEY_CID_PREFIX + id.getMccmncCarrierId()), id); 281 // priority: specific carrier id > carrier id > mccmnc fallback carrier id 282 if (!configBySpecificCarrierId.isEmpty()) { 283 config = configBySpecificCarrierId; 284 } else if (!configByCarrierId.isEmpty()) { 285 config = configByCarrierId; 286 } else if (!configByMccMncFallBackCarrierId.isEmpty()) { 287 config = configByMccMncFallBackCarrierId; 288 } 289 } 290 if (config.isEmpty()) { 291 // fallback to use mccmnc.xml when there is no carrier id named configuration found. 292 config = 293 parseCarrierConfigFromXml(assetsXmls.get(KEY_MCCMNC_PREFIX + carrier.getMccMnc()), id); 294 } 295 // Treat vendor.xml files as if they were appended to the carrier configs read from assets. 296 for (Document vendorXml : vendorXmls) { 297 HashMap<String, CarrierConfig.Config> vendorConfig = 298 parseCarrierConfigFromVendorXml(vendorXml, id); 299 config.putAll(vendorConfig); 300 } 301 302 rawConfigs.put(carrier, config); 303 } 304 305 // Read tier1_carriers.textpb 306 try (InputStream carriersTextpb = new FileInputStream(new File(carriersTextpbFile)); 307 BufferedReader br = new BufferedReader(new InputStreamReader(carriersTextpb, UTF_8))) { 308 CarrierList.Builder builder = CarrierList.newBuilder(); 309 TextFormat.getParser().merge(br, builder); 310 tier1Carriers = builder.build(); 311 } 312 313 // Compose tier1Configs and othersConfigs from rawConfigs 314 rawConfigs.forEach( 315 (carrierId, configs) -> { 316 String cname = getCanonicalName(tier1Carriers, carrierId); 317 CarrierConfig.Builder ccb = toCarrierConfigBuilder(configs); 318 if (cname != null) { // tier-1 carrier 319 if (tier1Configs.containsKey(cname)) { 320 tier1Configs.put( 321 cname, CarrierProtoUtils.mergeCarrierConfig(tier1Configs.get(cname), ccb)); 322 } else { 323 tier1Configs.put(cname, ccb.build()); 324 } 325 } else { // other carrier 326 cname = generateCanonicalNameForOthers(carrierId); 327 otherCarriers.add( 328 CarrierMap.newBuilder().addCarrierId(carrierId).setCanonicalName(cname).build()); 329 othersConfigs.put(cname, ccb.build()); 330 } 331 }); 332 333 // output tier1 carrier settings 334 for (int i = 0; i < tier1Carriers.getEntryCount(); i++) { 335 CarrierMap cm = tier1Carriers.getEntry(i); 336 String cname = cm.getCanonicalName(); 337 String fileName = getPathAsString(settingsTextpbDir, cname + ".textpb"); 338 339 outFiles.add(fileName); 340 341 try (OutputStream os = new FileOutputStream(new File(fileName)); 342 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, UTF_8))) { 343 CarrierSettings.Builder cs = CarrierSettings.newBuilder().setCanonicalName(cname); 344 if (tier1Configs.containsKey(cname)) { 345 cs.setConfigs(sortConfig(tier1Configs.get(cname)).toBuilder().build()); 346 } 347 cs.setVersion(version); 348 TextFormat.printUnicode(cs.build(), bw); 349 } 350 } 351 352 // Output other carriers list 353 String otherCarriersFile = getPathAsString(outputDir, "other_carriers.textpb"); 354 outFiles.add(otherCarriersFile); 355 CarrierProtoUtils.sortCarrierMapEntries(otherCarriers); 356 try (OutputStream os = new FileOutputStream(new File(otherCarriersFile)); 357 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, UTF_8))) { 358 CarrierList cl = 359 CarrierList.newBuilder().addAllEntry(otherCarriers).setVersion(version).build(); 360 TextFormat.printUnicode(cl, bw); 361 } 362 363 // Output other carriers settings 364 String othersFileName = getPathAsString(settingsTextpbDir, "others.textpb"); 365 outFiles.add(othersFileName); 366 try (OutputStream os = new FileOutputStream(new File(othersFileName)); 367 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, UTF_8))) { 368 MultiCarrierSettings.Builder mcs = MultiCarrierSettings.newBuilder().setVersion(version); 369 othersConfigs.forEach( 370 (cname, cc) -> { 371 mcs.addSetting( 372 CarrierSettings.newBuilder() 373 .setCanonicalName(cname) 374 .setConfigs(sortConfig(cc).toBuilder().build()) 375 .build()); 376 }); 377 TextFormat.printUnicode(mcs.build(), bw); 378 } 379 380 // Print out the list of all output file names 381 System.out.println("SUCCESS! Files generated:"); 382 for (String fileName : outFiles) { 383 System.out.println(fileName); 384 } 385 } 386 getDocumentBuilder()387 private static DocumentBuilder getDocumentBuilder() { 388 try { 389 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); 390 return dbFactory.newDocumentBuilder(); 391 } catch (ParserConfigurationException e) { 392 throw new IllegalStateException(e); 393 } 394 } 395 loadAospCarrierList()396 private static Multimap<Integer, CarrierId> loadAospCarrierList() throws IOException { 397 com.android.providers.telephony.CarrierIdProto.CarrierList.Builder aospCarrierList = 398 com.android.providers.telephony.CarrierIdProto.CarrierList.newBuilder(); 399 try (InputStream textpb = 400 CarrierConfigConverterV2.class.getResourceAsStream(RESOURCE_CARRIER_LIST); 401 BufferedReader textpbReader = new BufferedReader(new InputStreamReader(textpb, UTF_8))) { 402 TextFormat.getParser().merge(textpbReader, aospCarrierList); 403 } 404 return aospCarrierList.getCarrierIdList().stream() 405 .collect( 406 flatteningToMultimap( 407 cid -> cid.getCanonicalId(), 408 cid -> carrierAttributeToCarrierId(cid.getCarrierAttributeList()).stream(), 409 MultimapBuilder.linkedHashKeys().arrayListValues()::build)); 410 } 411 412 // Convert `CarrierAttribute`s to `CarrierId`s. 413 // A CarrierAttribute message with fields not supported by CarrierSettings, like preferred_apn, 414 // is ignored. carrierAttributeToCarrierId( List<CarrierAttribute> carrierAttributes)415 private static ImmutableList<CarrierId> carrierAttributeToCarrierId( 416 List<CarrierAttribute> carrierAttributes) { 417 List<CarrierId> result = new ArrayList<>(); 418 ImmutableSet<Descriptors.FieldDescriptor> supportedFields = 419 ImmutableSet.of( 420 CarrierAttribute.getDescriptor().findFieldByName("mccmnc_tuple"), 421 CarrierAttribute.getDescriptor().findFieldByName("imsi_prefix_xpattern"), 422 CarrierAttribute.getDescriptor().findFieldByName("spn"), 423 CarrierAttribute.getDescriptor().findFieldByName("gid1")); 424 for (CarrierAttribute carrierAttribute : carrierAttributes) { 425 if (!carrierAttribute.getAllFields().keySet().stream().allMatch(supportedFields::contains)) { 426 // This `CarrierAttribute` contains unsupported fields; skip. 427 continue; 428 } 429 for (String mccmnc : carrierAttribute.getMccmncTupleList()) { 430 CarrierId.Builder carrierId = CarrierId.newBuilder().setMccMnc(mccmnc); 431 if (carrierAttribute.getImsiPrefixXpatternCount() > 0) { 432 for (String imsi : carrierAttribute.getImsiPrefixXpatternList()) { 433 result.add(carrierId.setImsi(imsi).build()); 434 } 435 } else if (carrierAttribute.getGid1Count() > 0) { 436 for (String gid1 : carrierAttribute.getGid1List()) { 437 result.add(carrierId.setGid1(gid1).build()); 438 } 439 } else if (carrierAttribute.getSpnCount() > 0) { 440 for (String spn : carrierAttribute.getSpnList()) { 441 // Some SPN has trailng space character \r, messing up textpb. Remove them. 442 // It won't affect CarrierSettings which uses prefix matching for SPN. 443 result.add(carrierId.setSpn(CharMatcher.whitespace().trimTrailingFrom(spn)).build()); 444 } 445 } else { // Ignore other attributes not supported by CarrierSettings 446 result.add(carrierId.build()); 447 } 448 } 449 } 450 // Dedup 451 return ImmutableSet.copyOf(result).asList(); 452 } 453 reverseAospCarrierList( Multimap<Integer, CarrierId> aospCarrierList)454 private static Multimap<CarrierId, Integer> reverseAospCarrierList( 455 Multimap<Integer, CarrierId> aospCarrierList) { 456 return aospCarrierList.entries().stream() 457 .collect( 458 toMultimap( 459 entry -> entry.getValue(), 460 entry -> entry.getKey(), 461 MultimapBuilder.linkedHashKeys().arrayListValues()::build)); 462 } 463 parseXmlDoc(String fileName, DocumentBuilder xmlDocBuilder)464 private static Document parseXmlDoc(String fileName, DocumentBuilder xmlDocBuilder) 465 throws SAXException, IOException { 466 try (InputStream configXml = new FileInputStream(new File(fileName))) { 467 Document xmlDoc = xmlDocBuilder.parse(configXml); 468 xmlDoc.getDocumentElement().normalize(); 469 return xmlDoc; 470 } 471 } 472 getElementsByTagName(Document xmlDoc, String tagName)473 private static ImmutableList<Element> getElementsByTagName(Document xmlDoc, String tagName) { 474 if (xmlDoc == null) { 475 return ImmutableList.of(); 476 } 477 ImmutableList.Builder<Element> result = new ImmutableList.Builder<>(); 478 xmlDoc.getDocumentElement().normalize(); 479 NodeList nodeList = xmlDoc.getElementsByTagName(tagName); 480 for (int i = 0; i < nodeList.getLength(); i++) { 481 Node node = nodeList.item(i); 482 if (node.getNodeType() == Node.ELEMENT_NODE) { 483 result.add((Element) node); 484 } 485 } 486 return result.build(); 487 } 488 sortConfig(CarrierConfig in)489 static CarrierConfig sortConfig(CarrierConfig in) { 490 final CarrierConfig.Builder result = in.toBuilder().clearConfig(); 491 in.getConfigList().stream() 492 .sorted(comparing(CarrierConfig.Config::getKey)) 493 .forEachOrdered((c) -> result.addConfig(c)); 494 return result.build(); 495 } 496 getCanonicalName(CarrierList pList, CarrierId pId)497 static String getCanonicalName(CarrierList pList, CarrierId pId) { 498 for (int i = 0; i < pList.getEntryCount(); i++) { 499 CarrierMap cm = pList.getEntry(i); 500 for (int j = 0; j < cm.getCarrierIdCount(); j++) { 501 CarrierId cid = cm.getCarrierId(j); 502 if (cid.equals(pId)) { 503 return cm.getCanonicalName(); 504 } 505 } 506 } 507 return null; 508 } 509 generateCanonicalNameForOthers(CarrierId pId)510 static String generateCanonicalNameForOthers(CarrierId pId) { 511 // Not a tier-1 carrier: generate name 512 StringBuilder genName = new StringBuilder(pId.getMccMnc()); 513 switch (pId.getMvnoDataCase()) { 514 case GID1: 515 genName.append("GID1="); 516 genName.append(Ascii.toUpperCase(pId.getGid1())); 517 break; 518 case SPN: 519 genName.append("SPN="); 520 genName.append(Ascii.toUpperCase(pId.getSpn())); 521 break; 522 case IMSI: 523 genName.append("IMSI="); 524 genName.append(Ascii.toUpperCase(pId.getImsi())); 525 break; 526 default: // MVNODATA_NOT_SET 527 // Do nothing 528 } 529 return genName.toString(); 530 } 531 532 /** 533 * Converts a map with carrier configs to a {@link CarrierConfig.Builder}. 534 * 535 * @see #parseCarrierConfigToMap 536 */ toCarrierConfigBuilder( Map<String, CarrierConfig.Config> configs)537 private static CarrierConfig.Builder toCarrierConfigBuilder( 538 Map<String, CarrierConfig.Config> configs) { 539 CarrierConfig.Builder builder = CarrierConfig.newBuilder(); 540 configs.forEach( 541 (key, value) -> { 542 builder.addConfig(value.toBuilder().setKey(key)); 543 }); 544 return builder; 545 } 546 547 /** 548 * Returns a map with carrier configs parsed from a assets/*.xml. 549 * 550 * @return a map, key being the carrier config key, value being a {@link CarrierConfig.Config} 551 * with one of the value set. 552 */ parseCarrierConfigFromXml( Document xmlDoc, CarrierIdentifier carrier)553 private static HashMap<String, CarrierConfig.Config> parseCarrierConfigFromXml( 554 Document xmlDoc, CarrierIdentifier carrier) throws IOException { 555 HashMap<String, CarrierConfig.Config> configMap = new HashMap<>(); 556 for (Element element : getElementsByTagName(xmlDoc, TAG_CARRIER_CONFIG)) { 557 if (carrier != null && !checkFilters(false, element, carrier)) { 558 continue; 559 } 560 configMap.putAll(parseCarrierConfigToMap(element)); 561 } 562 return configMap; 563 } 564 565 /** 566 * Returns a map with carrier configs parsed from the vendor.xml. 567 * 568 * @return a map, key being the carrier config key, value being a {@link CarrierConfig.Config} 569 * with one of the value set. 570 */ parseCarrierConfigFromVendorXml( Document xmlDoc, CarrierIdentifier carrier)571 private static HashMap<String, CarrierConfig.Config> parseCarrierConfigFromVendorXml( 572 Document xmlDoc, CarrierIdentifier carrier) throws IOException { 573 HashMap<String, CarrierConfig.Config> configMap = new HashMap<>(); 574 for (Element element : getElementsByTagName(xmlDoc, TAG_CARRIER_CONFIG)) { 575 if (carrier != null && !checkFilters(true, element, carrier)) { 576 continue; 577 } 578 configMap.putAll(parseCarrierConfigToMap(element)); 579 } 580 return configMap; 581 } 582 583 /** 584 * Returns a map with carrier configs parsed from the XML element. 585 * 586 * @return a map, key being the carrier config key, value being a {@link CarrierConfig.Config} 587 * with one of the value set. 588 */ parseCarrierConfigToMap(Element element)589 private static HashMap<String, CarrierConfig.Config> parseCarrierConfigToMap(Element element) 590 throws IOException { 591 HashMap<String, CarrierConfig.Config> configMap = new HashMap<>(); 592 NodeList nList; 593 // bool value 594 nList = element.getElementsByTagName("boolean"); 595 for (int i = 0; i < nList.getLength(); i++) { 596 Node nNode = nList.item(i); 597 if (nNode.getNodeType() != Node.ELEMENT_NODE) { 598 continue; 599 } 600 Element eElement = (Element) nNode; 601 String key = eElement.getAttribute("name"); 602 boolean value = Boolean.parseBoolean(eElement.getAttribute("value")); 603 configMap.put(key, CarrierConfig.Config.newBuilder().setBoolValue(value).build()); 604 } 605 // int value 606 nList = element.getElementsByTagName("int"); 607 for (int i = 0; i < nList.getLength(); i++) { 608 Node nNode = nList.item(i); 609 if (nNode.getNodeType() != Node.ELEMENT_NODE) { 610 continue; 611 } 612 Element eElement = (Element) nNode; 613 String key = eElement.getAttribute("name"); 614 int value = Integer.parseInt(eElement.getAttribute("value")); 615 configMap.put(key, CarrierConfig.Config.newBuilder().setIntValue(value).build()); 616 } 617 // long value 618 nList = element.getElementsByTagName("long"); 619 for (int i = 0; i < nList.getLength(); i++) { 620 Node nNode = nList.item(i); 621 if (nNode.getNodeType() != Node.ELEMENT_NODE) { 622 continue; 623 } 624 Element eElement = (Element) nNode; 625 String key = eElement.getAttribute("name"); 626 long value = Long.parseLong(eElement.getAttribute("value")); 627 configMap.put(key, CarrierConfig.Config.newBuilder().setLongValue(value).build()); 628 } 629 // text value 630 nList = element.getElementsByTagName("string"); 631 for (int i = 0; i < nList.getLength(); i++) { 632 Node nNode = nList.item(i); 633 if (nNode.getNodeType() != Node.ELEMENT_NODE) { 634 continue; 635 } 636 Element eElement = (Element) nNode; 637 String key = eElement.getAttribute("name"); 638 String value = String.valueOf(eElement.getTextContent()); 639 configMap.put(key, CarrierConfig.Config.newBuilder().setTextValue(value).build()); 640 } 641 // text array 642 nList = element.getElementsByTagName("string-array"); 643 for (int i = 0; i < nList.getLength(); i++) { 644 Node nNode = nList.item(i); 645 if (nNode.getNodeType() != Node.ELEMENT_NODE) { 646 continue; 647 } 648 Element eElement = (Element) nNode; 649 String key = eElement.getAttribute("name"); 650 CarrierConfig.Config.Builder cccb = CarrierConfig.Config.newBuilder(); 651 TextArray.Builder cctb = TextArray.newBuilder(); 652 NodeList subList = eElement.getElementsByTagName("item"); 653 for (int j = 0; j < subList.getLength(); j++) { 654 Node subNode = subList.item(j); 655 if (subNode.getNodeType() != Node.ELEMENT_NODE) { 656 continue; 657 } 658 Element subElement = (Element) subNode; 659 String value = String.valueOf(subElement.getAttribute("value")); 660 cctb.addItem(value); 661 } 662 configMap.put(key, cccb.setTextArray(cctb.build()).build()); 663 } 664 // bool array 665 nList = element.getElementsByTagName("int-array"); 666 for (int i = 0; i < nList.getLength(); i++) { 667 Node nNode = nList.item(i); 668 if (nNode.getNodeType() != Node.ELEMENT_NODE) { 669 continue; 670 } 671 Element eElement = (Element) nNode; 672 String key = eElement.getAttribute("name"); 673 CarrierConfig.Config.Builder cccb = CarrierConfig.Config.newBuilder(); 674 IntArray.Builder ccib = IntArray.newBuilder(); 675 NodeList subList = eElement.getElementsByTagName("item"); 676 for (int j = 0; j < subList.getLength(); j++) { 677 Node subNode = subList.item(j); 678 if (subNode.getNodeType() != Node.ELEMENT_NODE) { 679 continue; 680 } 681 Element subElement = (Element) subNode; 682 int value = Integer.parseInt(subElement.getAttribute("value")); 683 ccib.addItem(value); 684 } 685 configMap.put(key, cccb.setIntArray(ccib.build()).build()); 686 } 687 return configMap; 688 } 689 690 /** 691 * Returns {@code true} if a <carrier_config ...> element matches the carrier identifier. 692 * 693 * <p>Copied from AOSP DefaultCarrierConfigService. 694 */ checkFilters(boolean isVendorXml, Element element, CarrierIdentifier id)695 private static boolean checkFilters(boolean isVendorXml, Element element, CarrierIdentifier id) { 696 // Special case: in vendor.xml, the <carrier_config> element w/o mcc/mnc provides a base config 697 // value for all carriers. CarrierSettings uses mcc/mnc 000/000 to identify that, and does the 698 // merge at run time instead of build time (which is now). 699 // Hence, such an element should only match 000/000. 700 if (isVendorXml 701 && !element.hasAttribute("mcc") 702 && !element.hasAttribute("mnc") 703 && !element.hasAttribute("cid")) { 704 return MCCMNC_FOR_DEFAULT_SETTINGS.equals(id.getMcc() + id.getMnc()); 705 } 706 boolean result = true; 707 NamedNodeMap attributes = element.getAttributes(); 708 for (int i = 0; i < attributes.getLength(); i++) { 709 String attribute = attributes.item(i).getNodeName(); 710 String value = attributes.item(i).getNodeValue(); 711 switch (attribute) { 712 case "mcc": 713 result = result && value.equals(id.getMcc()); 714 break; 715 case "mnc": 716 result = result && value.equals(id.getMnc()); 717 break; 718 case "gid1": 719 result = result && Ascii.equalsIgnoreCase(value, id.getGid1()); 720 break; 721 case "spn": 722 result = result && matchOnSP(value, id); 723 break; 724 case "imsi": 725 result = result && matchOnImsi(value, id); 726 break; 727 case "cid": 728 result = 729 result 730 && ((Integer.parseInt(value) == id.getCarrierId()) 731 || (Integer.parseInt(value) == id.getSpecificCarrierId())); 732 break; 733 case "name": 734 // name is used together with cid for readability. ignore for filter. 735 break; 736 default: 737 System.err.println("Unsupported attribute " + attribute + "=" + value); 738 result = false; 739 } 740 } 741 return result; 742 } 743 744 /** 745 * Returns {@code true} if an "spn" attribute in <carrier_config ...> element matches the carrier 746 * identifier. 747 * 748 * <p>Copied from AOSP DefaultCarrierConfigService. 749 */ matchOnSP(String xmlSP, CarrierIdentifier id)750 private static boolean matchOnSP(String xmlSP, CarrierIdentifier id) { 751 boolean matchFound = false; 752 753 String currentSP = id.getSpn(); 754 // <carrier_config ... spn="null"> means expecting SIM SPN empty in AOSP convention. 755 if (Ascii.equalsIgnoreCase("null", xmlSP)) { 756 if (currentSP.isEmpty()) { 757 matchFound = true; 758 } 759 } else if (currentSP != null) { 760 Pattern spPattern = Pattern.compile(xmlSP, Pattern.CASE_INSENSITIVE); 761 Matcher matcher = spPattern.matcher(currentSP); 762 matchFound = matcher.matches(); 763 } 764 return matchFound; 765 } 766 767 /** 768 * Returns {@code true} if an "imsi" attribute in <carrier_config ...> element matches the carrier 769 * identifier. 770 * 771 * <p>Copied from AOSP DefaultCarrierConfigService. 772 */ matchOnImsi(String xmlImsi, CarrierIdentifier id)773 private static boolean matchOnImsi(String xmlImsi, CarrierIdentifier id) { 774 boolean matchFound = false; 775 776 String currentImsi = id.getImsi(); 777 // If we were able to retrieve current IMSI, see if it matches. 778 if (currentImsi != null) { 779 Pattern imsiPattern = Pattern.compile(xmlImsi, Pattern.CASE_INSENSITIVE); 780 Matcher matcher = imsiPattern.matcher(currentImsi); 781 matchFound = matcher.matches(); 782 } 783 return matchFound; 784 } 785 786 /** 787 * Parses a {@link CarrierId} out of a <carrier_config ...> tag. 788 * 789 * <p>This is purely used for discover potential carriers expressed by this tag, the return value 790 * may not reflect all attributes of the tag. 791 */ parseCarrierId(Element element)792 private static CarrierId.Builder parseCarrierId(Element element) { 793 CarrierId.Builder builder = CarrierId.newBuilder(); 794 String mccMnc = element.getAttribute("mcc") + element.getAttribute("mnc"); 795 builder.setMccMnc(mccMnc); 796 if (element.hasAttribute("imsi")) { 797 builder.setImsi(element.getAttribute("imsi")); 798 } else if (element.hasAttribute("gid1")) { 799 builder.setGid1(element.getAttribute("gid1")); 800 } else if (element.hasAttribute("gid2")) { 801 throw new UnsupportedOperationException( 802 "Not support attribute `gid2`: " + element.getAttribute("gid2")); 803 } else if (element.hasAttribute("spn")) { 804 builder.setSpn(element.getAttribute("spn")); 805 } 806 return builder; 807 } 808 809 // Same as {@link java.nio.file.Paths#get} but returns a String getPathAsString(String first, String... more)810 private static String getPathAsString(String first, String... more) { 811 return java.nio.file.Paths.get(first, more).toString(); 812 } 813 814 /** Mirror of Android CarrierIdentifier class. Default value of a carrier id is -1. */ 815 @AutoValue 816 abstract static class CarrierIdentifier { getMcc()817 abstract String getMcc(); 818 getMnc()819 abstract String getMnc(); 820 getImsi()821 abstract String getImsi(); 822 getGid1()823 abstract String getGid1(); 824 getSpn()825 abstract String getSpn(); 826 getCarrierId()827 abstract int getCarrierId(); 828 getSpecificCarrierId()829 abstract int getSpecificCarrierId(); 830 getMccmncCarrierId()831 abstract int getMccmncCarrierId(); 832 create( CarrierId carrier, int carrierId, int specificCarrierId, int mccmncCarrierId)833 static CarrierIdentifier create( 834 CarrierId carrier, int carrierId, int specificCarrierId, int mccmncCarrierId) { 835 String mcc = carrier.getMccMnc().substring(0, 3); 836 String mnc = carrier.getMccMnc().length() > 3 ? carrier.getMccMnc().substring(3) : ""; 837 return new AutoValue_CarrierConfigConverterV2_CarrierIdentifier( 838 mcc, 839 mnc, 840 carrier.getImsi(), 841 carrier.getGid1(), 842 carrier.getSpn(), 843 carrierId, 844 specificCarrierId, 845 mccmncCarrierId); 846 } 847 } 848 getCid( CarrierId carrierId, Multimap<CarrierId, Integer> reverseAospCarrierList)849 private static CarrierIdentifier getCid( 850 CarrierId carrierId, Multimap<CarrierId, Integer> reverseAospCarrierList) { 851 // Mimic TelephonyManager#getCarrierIdFromMccMnc, which is implemented by 852 // CarrierResolver#getCarrierIdFromMccMnc. 853 CarrierId mccMnc = CarrierId.newBuilder().setMccMnc(carrierId.getMccMnc()).build(); 854 int mccMncCarrierId = reverseAospCarrierList.get(mccMnc).stream().findFirst().orElse(-1); 855 856 List<Integer> cids = ImmutableList.copyOf(reverseAospCarrierList.get(carrierId)); 857 // No match: use -1 858 if (cids.isEmpty()) { 859 return CarrierIdentifier.create(carrierId, -1, -1, mccMncCarrierId); 860 } 861 // One match: use as both carrierId and specificCarrierId 862 if (cids.size() == 1) { 863 return CarrierIdentifier.create(carrierId, cids.get(0), cids.get(0), mccMncCarrierId); 864 } 865 // Two matches: specificCarrierId is always bigger than carrierId 866 if (cids.size() == 2) { 867 return CarrierIdentifier.create( 868 carrierId, 869 Math.min(cids.get(0), cids.get(1)), 870 Math.max(cids.get(0), cids.get(1)), 871 mccMncCarrierId); 872 } 873 // Cannot be more than 2 matches. 874 throw new IllegalStateException("More than two cid's found for " + carrierId + ": " + cids); 875 } 876 CarrierConfigConverterV2()877 private CarrierConfigConverterV2() {} 878 } 879