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 @Parameter(names = "--consider_parent_canonical_id", arity = 1, description = "To consider parent_canonical_id") 106 private static boolean considerParentCanonicalId = false; 107 108 private static final String MCCMNC_FOR_DEFAULT_SETTINGS = "000000"; 109 110 // Resource file path to the AOSP carrier list file 111 private static final String RESOURCE_CARRIER_LIST = 112 "/assets/latest_carrier_id/carrier_list.textpb"; 113 114 // Constants used in parsing XMLs. 115 private static final String XML_SUFFIX = ".xml"; 116 private static final String CARRIER_CONFIG_MCCMNC_XML_PREFIX = "carrier_config_mccmnc_"; 117 private static final String CARRIER_CONFIG_CID_XML_PREFIX = "carrier_config_carrierid_"; 118 private static final String KEY_MCCMNC_PREFIX = "mccmnc_"; 119 private static final String KEY_CID_PREFIX = "cid_"; 120 private static final String TAG_CARRIER_CONFIG = "carrier_config"; 121 122 /** Entry point when invoked from command line. */ main(String[] args)123 public static void main(String[] args) throws IOException { 124 CarrierConfigConverterV2 converter = new CarrierConfigConverterV2(); 125 new JCommander(converter, args); 126 converter.convert(); 127 } 128 129 /** Entry point when invoked from other Java code, eg. the server side conversion tool. */ convert( String vendorXmlFile, String assetsDirName, String outputDir, long version, boolean considerParentCanonicalId)130 public static void convert( 131 String vendorXmlFile, String assetsDirName, String outputDir, long version, boolean considerParentCanonicalId) 132 throws IOException { 133 CarrierConfigConverterV2 converter = new CarrierConfigConverterV2(); 134 converter.vendorXmlFiles = ImmutableList.of(vendorXmlFile); 135 converter.assetsDirName = assetsDirName; 136 converter.outputDir = outputDir; 137 converter.version = version; 138 converter.considerParentCanonicalId = considerParentCanonicalId; 139 converter.convert(); 140 } 141 convert()142 private void convert() throws IOException { 143 String carriersTextpbFile = getPathAsString(outputDir, "tier1_carriers.textpb"); 144 String settingsTextpbDir = getPathAsString(outputDir, "setting"); 145 CarrierList tier1Carriers; 146 ArrayList<CarrierMap> otherCarriers = new ArrayList<>(); 147 ArrayList<String> outFiles = new ArrayList<>(); 148 HashMap<CarrierId, Map<String, CarrierConfig.Config>> rawConfigs = new HashMap<>(); 149 TreeMap<String, CarrierConfig> tier1Configs = new TreeMap<>(); 150 TreeMap<String, CarrierConfig> othersConfigs = new TreeMap<>(); 151 DocumentBuilder xmlDocBuilder = getDocumentBuilder(); 152 Multimap<Integer, CarrierId> aospCarrierList = loadAospCarrierList(); 153 Multimap<CarrierId, Integer> reverseAospCarrierList = reverseAospCarrierList(aospCarrierList); 154 Multimap<CarrierId, Integer> reverseAospCarrierListPerParentCanonicalId = reverseAospCarrierListPerParentCanonicalId(); 155 156 /* 157 * High-level flow: 158 * 1. Parse all input XMLs into memory 159 * 2. Collect a list of interested carriers from input, represented by CarrierId. 160 * 2. For each CarrierId, build its carreir configs, following AOSP DefaultCarrierConfigService. 161 * 3. Merge CarrierId's as per tier1_carriers.textpb 162 */ 163 164 // 1. Parse all input XMLs into memory 165 Map<String, Document> assetsXmls = new HashMap<>(); 166 List<Document> vendorXmls = new ArrayList<>(); 167 // Parse assets/carrier_config_*.xml 168 for (File childFile : new File(assetsDirName).listFiles()) { 169 String childFileName = childFile.getName(); 170 String fullChildName = childFile.getCanonicalPath(); 171 if (childFileName.startsWith(CARRIER_CONFIG_MCCMNC_XML_PREFIX)) { 172 String mccMnc = 173 childFileName.substring( 174 CARRIER_CONFIG_MCCMNC_XML_PREFIX.length(), childFileName.indexOf(XML_SUFFIX)); 175 if (!mccMnc.matches("\\d{5,6}")) { 176 throw new IOException("Invalid mcc/mnc " + mccMnc + " found in " + childFileName); 177 } 178 try { 179 assetsXmls.put(KEY_MCCMNC_PREFIX + mccMnc, parseXmlDoc(fullChildName, xmlDocBuilder)); 180 } catch (SAXException | IOException e) { 181 throw new IOException("Failed to parse " + childFileName, e); 182 } 183 } else if (childFileName.startsWith(CARRIER_CONFIG_CID_XML_PREFIX)) { 184 String cidAndCarrierName = 185 childFileName.substring( 186 CARRIER_CONFIG_CID_XML_PREFIX.length(), childFileName.indexOf(XML_SUFFIX)); 187 int cid = -1; 188 try { 189 cid = Integer.parseInt(cidAndCarrierName.split("_", -1)[0]); 190 } catch (NumberFormatException e) { 191 throw new IOException("Invalid carrierid found in " + childFileName, e); 192 } 193 try { 194 assetsXmls.put(KEY_CID_PREFIX + cid, parseXmlDoc(fullChildName, xmlDocBuilder)); 195 } catch (SAXException | IOException e) { 196 throw new IOException("Failed to parse " + childFileName, e); 197 } 198 } 199 // ignore other malformatted files. 200 } 201 // Parse vendor.xml files 202 for (String vendorXmlFile : vendorXmlFiles) { 203 try { 204 vendorXmls.add(parseXmlDoc(vendorXmlFile, xmlDocBuilder)); 205 } catch (SAXException | IOException e) { 206 throw new IOException("Failed to parse " + vendorXmlFile, e); 207 } 208 } 209 210 // 2. Collect all carriers from input, represented by CarrierId. 211 List<CarrierId> carriers = new ArrayList<>(); 212 // Traverse <carrier_config /> labels in each file. 213 for (Map.Entry<String, Document> xml : assetsXmls.entrySet()) { 214 if (xml.getKey().startsWith(KEY_MCCMNC_PREFIX)) { 215 String mccMnc = xml.getKey().substring(KEY_MCCMNC_PREFIX.length()); 216 for (Element element : getElementsByTagName(xml.getValue(), TAG_CARRIER_CONFIG)) { 217 try { 218 CarrierId id = parseCarrierId(element).setMccMnc(mccMnc).build(); 219 carriers.add(id); 220 } catch (UnsupportedOperationException e) { 221 throw new IOException("Unsupported syntax in assets/ for " + mccMnc, e); 222 } 223 } 224 } else if (xml.getKey().startsWith(KEY_CID_PREFIX)) { 225 int cid = Integer.parseInt(xml.getKey().substring(KEY_CID_PREFIX.length())); 226 if (aospCarrierList.containsKey(cid)) { 227 carriers.addAll(aospCarrierList.get(cid)); 228 } else { 229 System.err.printf("Undefined cid %d in assets/. Ignore.\n", cid); 230 } 231 } 232 } 233 for (Document vendorXml : vendorXmls) { 234 for (Element element : getElementsByTagName(vendorXml, TAG_CARRIER_CONFIG)) { 235 // First, try to parse cid 236 if (element.hasAttribute("cid")) { 237 String cidAsString = element.getAttribute("cid"); 238 int cid = Integer.parseInt(cidAsString); 239 if (aospCarrierList.containsKey(cid)) { 240 carriers.addAll(aospCarrierList.get(cid)); 241 } else { 242 System.err.printf("Undefined cid %d in vendor.xml. Ignore.\n", cid); 243 } 244 } else { 245 // Then, try to parse CarrierId 246 CarrierId.Builder id = parseCarrierId(element); 247 // A valid mccmnc is 5- or 6-digit. But vendor.xml see special cases below: 248 // Case 1: a <carrier_config> element may have neither "mcc" nor "mnc". 249 // Such a tag provides configs that should be applied to all carriers, including to 250 // unspecified carriers via the 000/000 default configs. Make sure 000/000 exists as 251 // a carrier. 252 // Case 2: a <carrier_config> element may have just "mcc" and not "mnc" for 253 // country-wise config. Such a element doesn't make a carrier; but still keep it so 254 // can be used if a mccmnc appears in APNs later. 255 if (id.getMccMnc().isEmpty()) { 256 // special case 1 257 carriers.add(id.setMccMnc(MCCMNC_FOR_DEFAULT_SETTINGS).build()); 258 } else if (id.getMccMnc().length() == 3) { 259 // special case 2 260 carriers.add(id.build()); 261 } else if (id.getMccMnc().length() == 5 || id.getMccMnc().length() == 6) { 262 // Normal mcc+mnc 263 carriers.add(id.build()); 264 } else { 265 System.err.printf("Invalid mcc/mnc: %s. Ignore.\n", id.getMccMnc()); 266 } 267 } 268 } 269 } 270 271 // 3. For each CarrierId, build its carrier configs, following AOSP DefaultCarrierConfigService. 272 loadUniqueRulesFromVendorXml(vendorXmls); 273 for (CarrierId carrier : carriers) { 274 Map<String, CarrierConfig.Config> config = ImmutableMap.of(); 275 276 CarrierIdentifier id = getCid(carrier, reverseAospCarrierList, reverseAospCarrierListPerParentCanonicalId); 277 if (id.getCarrierId() != -1) { 278 HashMap<String, CarrierConfig.Config> configBySpecificCarrierId = 279 parseCarrierConfigFromXml( 280 assetsXmls.get(KEY_CID_PREFIX + id.getSpecificCarrierId()), id); 281 HashMap<String, CarrierConfig.Config> configByCarrierId = 282 parseCarrierConfigFromXml(assetsXmls.get(KEY_CID_PREFIX + id.getCarrierId()), id); 283 HashMap<String, CarrierConfig.Config> configByMccMncFallBackCarrierId = 284 parseCarrierConfigFromXml(assetsXmls.get(KEY_CID_PREFIX + id.getMccmncCarrierId()), id); 285 // priority: specific carrier id > carrier id > mccmnc fallback carrier id 286 if (!configBySpecificCarrierId.isEmpty()) { 287 config = configBySpecificCarrierId; 288 } else if (!configByCarrierId.isEmpty()) { 289 config = configByCarrierId; 290 } else if (!configByMccMncFallBackCarrierId.isEmpty()) { 291 config = configByMccMncFallBackCarrierId; 292 } 293 } 294 if (config.isEmpty()) { 295 // fallback to use mccmnc.xml when there is no carrier id named configuration found. 296 config = 297 parseCarrierConfigFromXml(assetsXmls.get(KEY_MCCMNC_PREFIX + carrier.getMccMnc()), id); 298 } 299 // Treat vendor.xml files as if they were appended to the carrier configs read from assets. 300 for (Document vendorXml : vendorXmls) { 301 HashMap<String, CarrierConfig.Config> vendorConfig = 302 parseCarrierConfigFromVendorXml(vendorXml, id); 303 config.putAll(vendorConfig); 304 } 305 306 rawConfigs.put(carrier, config); 307 } 308 309 // Read tier1_carriers.textpb 310 try (InputStream carriersTextpb = new FileInputStream(new File(carriersTextpbFile)); 311 BufferedReader br = new BufferedReader(new InputStreamReader(carriersTextpb, UTF_8))) { 312 CarrierList.Builder builder = CarrierList.newBuilder(); 313 TextFormat.getParser().merge(br, builder); 314 tier1Carriers = builder.build(); 315 } 316 317 // Compose tier1Configs and othersConfigs from rawConfigs 318 rawConfigs.forEach( 319 (carrierId, configs) -> { 320 String cname = getCanonicalName(tier1Carriers, carrierId); 321 CarrierConfig.Builder ccb = toCarrierConfigBuilder(configs); 322 if (cname != null) { // tier-1 carrier 323 if (tier1Configs.containsKey(cname)) { 324 tier1Configs.put( 325 cname, CarrierProtoUtils.mergeCarrierConfig(tier1Configs.get(cname), ccb)); 326 } else { 327 tier1Configs.put(cname, ccb.build()); 328 } 329 } else { // other carrier 330 cname = generateCanonicalNameForOthers(carrierId); 331 otherCarriers.add( 332 CarrierMap.newBuilder().addCarrierId(carrierId).setCanonicalName(cname).build()); 333 othersConfigs.put(cname, ccb.build()); 334 } 335 }); 336 337 // output tier1 carrier settings 338 for (int i = 0; i < tier1Carriers.getEntryCount(); i++) { 339 CarrierMap cm = tier1Carriers.getEntry(i); 340 String cname = cm.getCanonicalName(); 341 String fileName = getPathAsString(settingsTextpbDir, cname + ".textpb"); 342 343 outFiles.add(fileName); 344 345 try (OutputStream os = new FileOutputStream(new File(fileName)); 346 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, UTF_8))) { 347 CarrierSettings.Builder cs = CarrierSettings.newBuilder().setCanonicalName(cname); 348 if (tier1Configs.containsKey(cname)) { 349 cs.setConfigs(sortConfig(tier1Configs.get(cname)).toBuilder().build()); 350 } 351 cs.setVersion(version); 352 TextFormat.printUnicode(cs.build(), bw); 353 } 354 } 355 356 // Output other carriers list 357 String otherCarriersFile = getPathAsString(outputDir, "other_carriers.textpb"); 358 outFiles.add(otherCarriersFile); 359 CarrierProtoUtils.sortCarrierMapEntries(otherCarriers); 360 try (OutputStream os = new FileOutputStream(new File(otherCarriersFile)); 361 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, UTF_8))) { 362 CarrierList cl = 363 CarrierList.newBuilder().addAllEntry(otherCarriers).setVersion(version).build(); 364 TextFormat.printUnicode(cl, bw); 365 } 366 367 // Output other carriers settings 368 String othersFileName = getPathAsString(settingsTextpbDir, "others.textpb"); 369 outFiles.add(othersFileName); 370 try (OutputStream os = new FileOutputStream(new File(othersFileName)); 371 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, UTF_8))) { 372 MultiCarrierSettings.Builder mcs = MultiCarrierSettings.newBuilder().setVersion(version); 373 othersConfigs.forEach( 374 (cname, cc) -> { 375 mcs.addSetting( 376 CarrierSettings.newBuilder() 377 .setCanonicalName(cname) 378 .setConfigs(sortConfig(cc).toBuilder().build()) 379 .build()); 380 }); 381 TextFormat.printUnicode(mcs.build(), bw); 382 } 383 384 // Print out the list of all output file names 385 System.out.println("SUCCESS! Files generated:"); 386 for (String fileName : outFiles) { 387 System.out.println(fileName); 388 } 389 } 390 getDocumentBuilder()391 private static DocumentBuilder getDocumentBuilder() { 392 try { 393 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); 394 return dbFactory.newDocumentBuilder(); 395 } catch (ParserConfigurationException e) { 396 throw new IllegalStateException(e); 397 } 398 } 399 loadAospCarrierList()400 private static Multimap<Integer, CarrierId> loadAospCarrierList() throws IOException { 401 com.android.providers.telephony.CarrierIdProto.CarrierList.Builder aospCarrierList = 402 com.android.providers.telephony.CarrierIdProto.CarrierList.newBuilder(); 403 try (InputStream textpb = 404 CarrierConfigConverterV2.class.getResourceAsStream(RESOURCE_CARRIER_LIST); 405 BufferedReader textpbReader = new BufferedReader(new InputStreamReader(textpb, UTF_8))) { 406 TextFormat.getParser().merge(textpbReader, aospCarrierList); 407 } 408 return aospCarrierList.getCarrierIdList().stream() 409 .collect( 410 flatteningToMultimap( 411 cid -> cid.getCanonicalId(), 412 cid -> carrierAttributeToCarrierId(cid.getCarrierAttributeList()).stream(), 413 MultimapBuilder.linkedHashKeys().arrayListValues()::build)); 414 } 415 reverseAospCarrierListPerParentCanonicalId()416 private static Multimap<CarrierId, Integer> reverseAospCarrierListPerParentCanonicalId() throws IOException { 417 418 com.android.providers.telephony.CarrierIdProto.CarrierList.Builder aospCarrierList = 419 com.android.providers.telephony.CarrierIdProto.CarrierList.newBuilder(); 420 try (InputStream textpb = 421 CarrierConfigConverterV2.class.getResourceAsStream(RESOURCE_CARRIER_LIST); 422 BufferedReader textpbReader = new BufferedReader(new InputStreamReader(textpb, UTF_8))) { 423 TextFormat.getParser().merge(textpbReader, aospCarrierList); 424 } 425 Multimap<Integer, CarrierId> res = aospCarrierList.getCarrierIdList().stream() 426 .filter(cid -> cid.getParentCanonicalId() > 0) 427 .collect( 428 flatteningToMultimap( 429 cid -> cid.getParentCanonicalId(), 430 cid -> carrierAttributeToCarrierId(cid.getCarrierAttributeList()).stream(), 431 MultimapBuilder.linkedHashKeys().arrayListValues()::build)); 432 433 return res.entries().stream() 434 .collect( 435 toMultimap( 436 entry -> entry.getValue(), 437 entry -> entry.getKey(), 438 MultimapBuilder.linkedHashKeys().arrayListValues()::build)); 439 } 440 441 // Convert `CarrierAttribute`s to `CarrierId`s. 442 // A CarrierAttribute message with fields not supported by CarrierSettings, like preferred_apn, 443 // is ignored. carrierAttributeToCarrierId( List<CarrierAttribute> carrierAttributes)444 private static ImmutableList<CarrierId> carrierAttributeToCarrierId( 445 List<CarrierAttribute> carrierAttributes) { 446 List<CarrierId> result = new ArrayList<>(); 447 ImmutableSet<Descriptors.FieldDescriptor> supportedFields = 448 ImmutableSet.of( 449 CarrierAttribute.getDescriptor().findFieldByName("mccmnc_tuple"), 450 CarrierAttribute.getDescriptor().findFieldByName("imsi_prefix_xpattern"), 451 CarrierAttribute.getDescriptor().findFieldByName("spn"), 452 CarrierAttribute.getDescriptor().findFieldByName("gid1")); 453 for (CarrierAttribute carrierAttribute : carrierAttributes) { 454 if (!carrierAttribute.getAllFields().keySet().stream().allMatch(supportedFields::contains)) { 455 // This `CarrierAttribute` contains unsupported fields; skip. 456 continue; 457 } 458 for (String mccmnc : carrierAttribute.getMccmncTupleList()) { 459 CarrierId.Builder carrierId = CarrierId.newBuilder().setMccMnc(mccmnc); 460 if (carrierAttribute.getImsiPrefixXpatternCount() > 0) { 461 for (String imsi : carrierAttribute.getImsiPrefixXpatternList()) { 462 result.add(carrierId.setImsi(imsi).build()); 463 } 464 } else if (carrierAttribute.getGid1Count() > 0) { 465 for (String gid1 : carrierAttribute.getGid1List()) { 466 result.add(carrierId.setGid1(gid1).build()); 467 } 468 } else if (carrierAttribute.getSpnCount() > 0) { 469 for (String spn : carrierAttribute.getSpnList()) { 470 // Some SPN has trailng space character \r, messing up textpb. Remove them. 471 // It won't affect CarrierSettings which uses prefix matching for SPN. 472 result.add(carrierId.setSpn(CharMatcher.whitespace().trimTrailingFrom(spn)).build()); 473 } 474 } else { // Ignore other attributes not supported by CarrierSettings 475 result.add(carrierId.build()); 476 } 477 } 478 } 479 // Dedup 480 return ImmutableSet.copyOf(result).asList(); 481 } 482 reverseAospCarrierList( Multimap<Integer, CarrierId> aospCarrierList)483 private static Multimap<CarrierId, Integer> reverseAospCarrierList( 484 Multimap<Integer, CarrierId> aospCarrierList) { 485 return aospCarrierList.entries().stream() 486 .collect( 487 toMultimap( 488 entry -> entry.getValue(), 489 entry -> entry.getKey(), 490 MultimapBuilder.linkedHashKeys().arrayListValues()::build)); 491 } 492 parseXmlDoc(String fileName, DocumentBuilder xmlDocBuilder)493 private static Document parseXmlDoc(String fileName, DocumentBuilder xmlDocBuilder) 494 throws SAXException, IOException { 495 try (InputStream configXml = new FileInputStream(new File(fileName))) { 496 Document xmlDoc = xmlDocBuilder.parse(configXml); 497 xmlDoc.getDocumentElement().normalize(); 498 return xmlDoc; 499 } 500 } 501 getElementsByTagName(Document xmlDoc, String tagName)502 private static ImmutableList<Element> getElementsByTagName(Document xmlDoc, String tagName) { 503 if (xmlDoc == null) { 504 return ImmutableList.of(); 505 } 506 ImmutableList.Builder<Element> result = new ImmutableList.Builder<>(); 507 xmlDoc.getDocumentElement().normalize(); 508 NodeList nodeList = xmlDoc.getElementsByTagName(tagName); 509 for (int i = 0; i < nodeList.getLength(); i++) { 510 Node node = nodeList.item(i); 511 if (node.getNodeType() == Node.ELEMENT_NODE) { 512 result.add((Element) node); 513 } 514 } 515 return result.build(); 516 } 517 sortConfig(CarrierConfig in)518 static CarrierConfig sortConfig(CarrierConfig in) { 519 final CarrierConfig.Builder result = in.toBuilder().clearConfig(); 520 in.getConfigList().stream() 521 .sorted(comparing(CarrierConfig.Config::getKey)) 522 .forEachOrdered((c) -> result.addConfig(c)); 523 return result.build(); 524 } 525 getCanonicalName(CarrierList pList, CarrierId pId)526 static String getCanonicalName(CarrierList pList, CarrierId pId) { 527 for (int i = 0; i < pList.getEntryCount(); i++) { 528 CarrierMap cm = pList.getEntry(i); 529 for (int j = 0; j < cm.getCarrierIdCount(); j++) { 530 CarrierId cid = cm.getCarrierId(j); 531 if (cid.equals(pId)) { 532 return cm.getCanonicalName(); 533 } 534 } 535 } 536 return null; 537 } 538 generateCanonicalNameForOthers(CarrierId pId)539 static String generateCanonicalNameForOthers(CarrierId pId) { 540 // Not a tier-1 carrier: generate name 541 StringBuilder genName = new StringBuilder(pId.getMccMnc()); 542 switch (pId.getMvnoDataCase()) { 543 case GID1: 544 genName.append("GID1="); 545 genName.append(Ascii.toUpperCase(pId.getGid1())); 546 break; 547 case SPN: 548 genName.append("SPN="); 549 genName.append(Ascii.toUpperCase(pId.getSpn())); 550 break; 551 case IMSI: 552 genName.append("IMSI="); 553 genName.append(Ascii.toUpperCase(pId.getImsi())); 554 break; 555 default: // MVNODATA_NOT_SET 556 // Do nothing 557 } 558 return genName.toString(); 559 } 560 561 /** 562 * Converts a map with carrier configs to a {@link CarrierConfig.Builder}. 563 * 564 * @see #parseCarrierConfigToMap 565 */ toCarrierConfigBuilder( Map<String, CarrierConfig.Config> configs)566 private static CarrierConfig.Builder toCarrierConfigBuilder( 567 Map<String, CarrierConfig.Config> configs) { 568 CarrierConfig.Builder builder = CarrierConfig.newBuilder(); 569 configs.forEach( 570 (key, value) -> { 571 builder.addConfig(value.toBuilder().setKey(key)); 572 }); 573 return builder; 574 } 575 576 /** 577 * Returns a map with carrier configs parsed from a assets/*.xml. 578 * 579 * @return a map, key being the carrier config key, value being a {@link CarrierConfig.Config} 580 * with one of the value set. 581 */ parseCarrierConfigFromXml( Document xmlDoc, CarrierIdentifier carrier)582 private static HashMap<String, CarrierConfig.Config> parseCarrierConfigFromXml( 583 Document xmlDoc, CarrierIdentifier carrier) throws IOException { 584 HashMap<String, CarrierConfig.Config> configMap = new HashMap<>(); 585 for (Element element : getElementsByTagName(xmlDoc, TAG_CARRIER_CONFIG)) { 586 if (carrier != null && !checkFilters(element, carrier)) { 587 continue; 588 } 589 configMap.putAll(parseCarrierConfigToMap(element)); 590 } 591 return configMap; 592 } 593 594 /** 595 * Returns a map with carrier configs parsed from the vendor.xml. 596 * 597 * @return a map, key being the carrier config key, value being a {@link CarrierConfig.Config} 598 * with one of the value set. 599 */ parseCarrierConfigFromVendorXml( Document xmlDoc, CarrierIdentifier carrier)600 private HashMap<String, CarrierConfig.Config> parseCarrierConfigFromVendorXml( 601 Document xmlDoc, CarrierIdentifier carrier) throws IOException { 602 HashMap<String, CarrierConfig.Config> configMap = new HashMap<>(); 603 for (Element element : getElementsByTagName(xmlDoc, TAG_CARRIER_CONFIG)) { 604 if (carrier != null && !checkFilters(element, carrier)) { 605 continue; 606 } 607 608 Element parent_config = findParentConfigByUniqueRuleId(element); 609 if (parent_config != null) { 610 configMap.putAll(parseCarrierConfigToMap(parent_config)); 611 } 612 613 configMap.putAll(parseCarrierConfigToMap(element)); 614 } 615 return configMap; 616 } 617 618 /** 619 * Returns a map with carrier configs parsed from the XML element. 620 * 621 * @return a map, key being the carrier config key, value being a {@link CarrierConfig.Config} 622 * with one of the value set. 623 */ parseCarrierConfigToMap(Element element)624 private static HashMap<String, CarrierConfig.Config> parseCarrierConfigToMap(Element element) 625 throws IOException { 626 HashMap<String, CarrierConfig.Config> configMap = new HashMap<>(); 627 NodeList nList; 628 // bool value 629 nList = element.getElementsByTagName("boolean"); 630 for (int i = 0; i < nList.getLength(); i++) { 631 Node nNode = nList.item(i); 632 if (nNode.getNodeType() != Node.ELEMENT_NODE || 633 !nNode.getParentNode().isSameNode(element)) { 634 continue; 635 } 636 Element eElement = (Element) nNode; 637 String key = eElement.getAttribute("name"); 638 boolean value = Boolean.parseBoolean(eElement.getAttribute("value")); 639 configMap.put(key, CarrierConfig.Config.newBuilder().setBoolValue(value).build()); 640 } 641 // int value 642 nList = element.getElementsByTagName("int"); 643 for (int i = 0; i < nList.getLength(); i++) { 644 Node nNode = nList.item(i); 645 if (nNode.getNodeType() != Node.ELEMENT_NODE || 646 !nNode.getParentNode().isSameNode(element)) { 647 continue; 648 } 649 Element eElement = (Element) nNode; 650 String key = eElement.getAttribute("name"); 651 int value = Integer.parseInt(eElement.getAttribute("value")); 652 configMap.put(key, CarrierConfig.Config.newBuilder().setIntValue(value).build()); 653 } 654 // long value 655 nList = element.getElementsByTagName("long"); 656 for (int i = 0; i < nList.getLength(); i++) { 657 Node nNode = nList.item(i); 658 if (nNode.getNodeType() != Node.ELEMENT_NODE || 659 !nNode.getParentNode().isSameNode(element)) { 660 continue; 661 } 662 Element eElement = (Element) nNode; 663 String key = eElement.getAttribute("name"); 664 long value = Long.parseLong(eElement.getAttribute("value")); 665 configMap.put(key, CarrierConfig.Config.newBuilder().setLongValue(value).build()); 666 } 667 // double value 668 nList = element.getElementsByTagName("double"); 669 for (int i = 0; i < nList.getLength(); i++) { 670 Node nNode = nList.item(i); 671 if (nNode.getNodeType() != Node.ELEMENT_NODE || 672 !nNode.getParentNode().isSameNode(element)) { 673 continue; 674 } 675 Element eElement = (Element) nNode; 676 String key = eElement.getAttribute("name"); 677 double value = Double.parseDouble(eElement.getAttribute("value")); 678 configMap.put(key, CarrierConfig.Config.newBuilder().setDoubleValue(value).build()); 679 } 680 // text value 681 nList = element.getElementsByTagName("string"); 682 for (int i = 0; i < nList.getLength(); i++) { 683 Node nNode = nList.item(i); 684 if (nNode.getNodeType() != Node.ELEMENT_NODE || 685 !nNode.getParentNode().isSameNode(element)) { 686 continue; 687 } 688 Element eElement = (Element) nNode; 689 String key = eElement.getAttribute("name"); 690 String value = String.valueOf(eElement.getTextContent()); 691 if (value.isEmpty()) { 692 value = eElement.getAttribute("value"); 693 } 694 configMap.put(key, CarrierConfig.Config.newBuilder().setTextValue(value).build()); 695 } 696 // text array 697 nList = element.getElementsByTagName("string-array"); 698 for (int i = 0; i < nList.getLength(); i++) { 699 Node nNode = nList.item(i); 700 if (nNode.getNodeType() != Node.ELEMENT_NODE || 701 !nNode.getParentNode().isSameNode(element)) { 702 continue; 703 } 704 Element eElement = (Element) nNode; 705 String key = eElement.getAttribute("name"); 706 CarrierConfig.Config.Builder cccb = CarrierConfig.Config.newBuilder(); 707 TextArray.Builder cctb = TextArray.newBuilder(); 708 NodeList subList = eElement.getElementsByTagName("item"); 709 for (int j = 0; j < subList.getLength(); j++) { 710 Node subNode = subList.item(j); 711 if (subNode.getNodeType() != Node.ELEMENT_NODE) { 712 continue; 713 } 714 Element subElement = (Element) subNode; 715 String value = String.valueOf(subElement.getAttribute("value")); 716 cctb.addItem(value); 717 } 718 configMap.put(key, cccb.setTextArray(cctb.build()).build()); 719 } 720 // int array 721 nList = element.getElementsByTagName("int-array"); 722 for (int i = 0; i < nList.getLength(); i++) { 723 Node nNode = nList.item(i); 724 if (nNode.getNodeType() != Node.ELEMENT_NODE || 725 !nNode.getParentNode().isSameNode(element)) { 726 continue; 727 } 728 Element eElement = (Element) nNode; 729 String key = eElement.getAttribute("name"); 730 CarrierConfig.Config.Builder cccb = CarrierConfig.Config.newBuilder(); 731 IntArray.Builder ccib = IntArray.newBuilder(); 732 NodeList subList = eElement.getElementsByTagName("item"); 733 for (int j = 0; j < subList.getLength(); j++) { 734 Node subNode = subList.item(j); 735 if (subNode.getNodeType() != Node.ELEMENT_NODE) { 736 continue; 737 } 738 Element subElement = (Element) subNode; 739 int value = Integer.parseInt(subElement.getAttribute("value")); 740 ccib.addItem(value); 741 } 742 configMap.put(key, cccb.setIntArray(ccib.build()).build()); 743 } 744 // pbundle_as_map 745 nList = element.getElementsByTagName("pbundle_as_map"); 746 for (int i = 0; i < nList.getLength(); i++) { 747 Node nNode = nList.item(i); 748 if (nNode.getNodeType() != Node.ELEMENT_NODE || 749 !nNode.getParentNode().isSameNode(element)) { 750 continue; 751 } 752 Element eElement = (Element) nNode; 753 String key = eElement.getAttribute("name"); 754 HashMap<String, CarrierConfig.Config> value = parseCarrierConfigToMap(eElement); 755 configMap.put(key, CarrierConfig.Config.newBuilder() 756 .setBundle(toCarrierConfigBuilder(value)).build()); 757 } 758 return configMap; 759 } 760 761 /** 762 * Returns {@code true} if a <carrier_config ...> element matches the carrier identifier. 763 * 764 * <p>Copied from AOSP DefaultCarrierConfigService. 765 */ checkFilters(Element element, CarrierIdentifier id)766 private static boolean checkFilters(Element element, CarrierIdentifier id) { 767 boolean result = true; 768 NamedNodeMap attributes = element.getAttributes(); 769 for (int i = 0; i < attributes.getLength(); i++) { 770 String attribute = attributes.item(i).getNodeName(); 771 String value = attributes.item(i).getNodeValue(); 772 switch (attribute) { 773 case "mcc": 774 result = result && value.equals(id.getMcc()); 775 break; 776 case "mnc": 777 result = result && value.equals(id.getMnc()); 778 break; 779 case "gid1": 780 result = result && Ascii.equalsIgnoreCase(value, id.getGid1()); 781 break; 782 case "spn": 783 result = result && matchOnSP(value, id); 784 break; 785 case "imsi": 786 result = result && matchOnImsi(value, id); 787 break; 788 case "cid": 789 result = 790 result 791 && ((Integer.parseInt(value) == id.getCarrierId()) 792 || (Integer.parseInt(value) == id.getSpecificCarrierId())); 793 break; 794 case "name": 795 // name is used together with cid for readability. ignore for filter. 796 case "unique_rule_id": 797 case "following": 798 break; 799 default: 800 System.err.println("Unsupported attribute " + attribute + "=" + value); 801 result = false; 802 } 803 } 804 return result; 805 } 806 807 /** 808 * Returns {@code true} if an "spn" attribute in <carrier_config ...> element matches the carrier 809 * identifier. 810 * 811 * <p>Copied from AOSP DefaultCarrierConfigService. 812 */ matchOnSP(String xmlSP, CarrierIdentifier id)813 private static boolean matchOnSP(String xmlSP, CarrierIdentifier id) { 814 boolean matchFound = false; 815 816 String currentSP = id.getSpn(); 817 // <carrier_config ... spn="null"> means expecting SIM SPN empty in AOSP convention. 818 if (Ascii.equalsIgnoreCase("null", xmlSP)) { 819 if (currentSP.isEmpty()) { 820 matchFound = true; 821 } 822 } else if (currentSP != null) { 823 Pattern spPattern = Pattern.compile(xmlSP, Pattern.CASE_INSENSITIVE); 824 Matcher matcher = spPattern.matcher(currentSP); 825 matchFound = matcher.matches(); 826 } 827 return matchFound; 828 } 829 830 /** 831 * Returns {@code true} if an "imsi" attribute in <carrier_config ...> element matches the carrier 832 * identifier. 833 * 834 * <p>Copied from AOSP DefaultCarrierConfigService. 835 */ matchOnImsi(String xmlImsi, CarrierIdentifier id)836 private static boolean matchOnImsi(String xmlImsi, CarrierIdentifier id) { 837 boolean matchFound = false; 838 839 String currentImsi = id.getImsi(); 840 // If we were able to retrieve current IMSI, see if it matches. 841 if (currentImsi != null) { 842 Pattern imsiPattern = Pattern.compile(xmlImsi, Pattern.CASE_INSENSITIVE); 843 Matcher matcher = imsiPattern.matcher(currentImsi); 844 matchFound = matcher.matches(); 845 } 846 return matchFound; 847 } 848 849 /** 850 * Parses a {@link CarrierId} out of a <carrier_config ...> tag. 851 * 852 * <p>This is purely used for discover potential carriers expressed by this tag, the return value 853 * may not reflect all attributes of the tag. 854 */ parseCarrierId(Element element)855 private static CarrierId.Builder parseCarrierId(Element element) { 856 CarrierId.Builder builder = CarrierId.newBuilder(); 857 String mccMnc = element.getAttribute("mcc") + element.getAttribute("mnc"); 858 builder.setMccMnc(mccMnc); 859 if (element.hasAttribute("imsi")) { 860 builder.setImsi(element.getAttribute("imsi")); 861 } else if (element.hasAttribute("gid1")) { 862 builder.setGid1(element.getAttribute("gid1")); 863 } else if (element.hasAttribute("gid2")) { 864 throw new UnsupportedOperationException( 865 "Not support attribute `gid2`: " + element.getAttribute("gid2")); 866 } else if (element.hasAttribute("spn")) { 867 builder.setSpn(element.getAttribute("spn")); 868 } 869 return builder; 870 } 871 872 // Same as {@link java.nio.file.Paths#get} but returns a String getPathAsString(String first, String... more)873 private static String getPathAsString(String first, String... more) { 874 return java.nio.file.Paths.get(first, more).toString(); 875 } 876 877 /** Mirror of Android CarrierIdentifier class. Default value of a carrier id is -1. */ 878 @AutoValue 879 abstract static class CarrierIdentifier { getMcc()880 abstract String getMcc(); 881 getMnc()882 abstract String getMnc(); 883 getImsi()884 abstract String getImsi(); 885 getGid1()886 abstract String getGid1(); 887 getSpn()888 abstract String getSpn(); 889 getCarrierId()890 abstract int getCarrierId(); 891 getSpecificCarrierId()892 abstract int getSpecificCarrierId(); 893 getMccmncCarrierId()894 abstract int getMccmncCarrierId(); 895 create( CarrierId carrier, int carrierId, int specificCarrierId, int mccmncCarrierId)896 static CarrierIdentifier create( 897 CarrierId carrier, int carrierId, int specificCarrierId, int mccmncCarrierId) { 898 String mcc = carrier.getMccMnc().substring(0, 3); 899 String mnc = carrier.getMccMnc().length() > 3 ? carrier.getMccMnc().substring(3) : ""; 900 return new AutoValue_CarrierConfigConverterV2_CarrierIdentifier( 901 mcc, 902 mnc, 903 carrier.getImsi(), 904 carrier.getGid1(), 905 carrier.getSpn(), 906 carrierId, 907 specificCarrierId, 908 mccmncCarrierId); 909 } 910 } 911 getCid( CarrierId carrierId, Multimap<CarrierId, Integer> reverseAospCarrierList, Multimap<CarrierId, Integer> reverseAospCarrierListPerParentCanonicalId)912 private static CarrierIdentifier getCid( 913 CarrierId carrierId, Multimap<CarrierId, Integer> reverseAospCarrierList, 914 Multimap<CarrierId, Integer> reverseAospCarrierListPerParentCanonicalId) { 915 // Mimic TelephonyManager#getCarrierIdFromMccMnc, which is implemented by 916 // CarrierResolver#getCarrierIdFromMccMnc. 917 CarrierId mccMnc = CarrierId.newBuilder().setMccMnc(carrierId.getMccMnc()).build(); 918 int mccMncCarrierId = reverseAospCarrierList.get(mccMnc).stream().findFirst().orElse(-1); 919 List<Integer> cids = ImmutableList.copyOf(reverseAospCarrierList.get(carrierId)); 920 int parentCanonicalId = getParentCanonicalId(carrierId, cids, reverseAospCarrierListPerParentCanonicalId); 921 // No match: use -1 922 if (cids.isEmpty()) { 923 if (considerParentCanonicalId) { 924 return CarrierIdentifier.create(carrierId, parentCanonicalId, -1, mccMncCarrierId); 925 } else { 926 return CarrierIdentifier.create(carrierId, -1, -1, mccMncCarrierId); 927 } 928 } 929 // One match: use as both carrierId and specificCarrierId 930 if (cids.size() == 1) { 931 if (considerParentCanonicalId) { 932 return CarrierIdentifier.create(carrierId, parentCanonicalId, cids.get(0), mccMncCarrierId); 933 } else { 934 return CarrierIdentifier.create(carrierId, cids.get(0), cids.get(0), mccMncCarrierId); 935 } 936 } 937 // Two matches: specificCarrierId is always bigger than carrierId 938 if (cids.size() == 2) { 939 if (considerParentCanonicalId) { 940 return CarrierIdentifier.create( 941 carrierId, 942 parentCanonicalId, 943 Math.max(cids.get(0), cids.get(1)), 944 mccMncCarrierId); 945 } else { 946 return CarrierIdentifier.create( 947 carrierId, 948 Math.min(cids.get(0), cids.get(1)), 949 Math.max(cids.get(0), cids.get(1)), 950 mccMncCarrierId); 951 } 952 } 953 // Cannot be more than 2 matches. 954 throw new IllegalStateException("More than two cid's found for " + carrierId + ": " + cids); 955 } 956 getParentCanonicalId( CarrierId carrierId, List<Integer> cids, Multimap<CarrierId, Integer> reverseAospCarrierListPerParentCanonicalId)957 private static int getParentCanonicalId( 958 CarrierId carrierId, 959 List<Integer> cids, 960 Multimap<CarrierId, Integer> reverseAospCarrierListPerParentCanonicalId) { 961 962 List<Integer> parentCids = ImmutableList.copyOf(reverseAospCarrierListPerParentCanonicalId.get(carrierId)); 963 if (cids.isEmpty()) { 964 if (parentCids.isEmpty()) { 965 return -1; 966 } else { 967 return parentCids.get(0); 968 } 969 } else if (cids.size() == 1) { 970 if (parentCids.isEmpty()) { 971 return cids.get(0); 972 } else { 973 return parentCids.get(0); 974 } 975 } else if (cids.size() == 2) { 976 if (parentCids.isEmpty()) { 977 return Math.min(cids.get(0), cids.get(1)); 978 } else { 979 return parentCids.get(0); 980 } 981 } else { 982 return -1; 983 } 984 } CarrierConfigConverterV2()985 private CarrierConfigConverterV2() {} 986 987 // The hash map to store all the configs with attribute "unique_rule_id". 988 // The config entry with attribute "following" can inherit from the config 989 // with matching "unique_rule_id". 990 // Both "unique_rule_id" and "following" attributes can only appear in vendor xml. 991 private HashMap<String, Element> mUniqueRules = new HashMap<>(); 992 loadUniqueRulesFromVendorXml(List<Document> vendorXmls)993 private void loadUniqueRulesFromVendorXml(List<Document> vendorXmls) 994 throws IOException { 995 for (Document vendorXml : vendorXmls) { 996 for (Element element : getElementsByTagName(vendorXml, TAG_CARRIER_CONFIG)) { 997 NamedNodeMap attributes = element.getAttributes(); 998 boolean uniqueRuleIdSeen = false; 999 for (int i = 0; i < attributes.getLength(); i++) { 1000 String attribute = attributes.item(i).getNodeName(); 1001 String value = attributes.item(i).getNodeValue(); 1002 switch (attribute) { 1003 case "unique_rule_id": 1004 if (mUniqueRules.containsKey(value)) { 1005 throw new IOException("The carrier_config has duplicated unique_rule_id: " + attributes); 1006 } else if (uniqueRuleIdSeen) { 1007 throw new IOException("The carrier_config has more than 1 unique_rule_id: " + attributes); 1008 } 1009 mUniqueRules.put(value, element); 1010 uniqueRuleIdSeen = true; 1011 break; 1012 default: 1013 break; 1014 } 1015 } 1016 } 1017 } 1018 } 1019 findParentConfigByUniqueRuleId(Element childElement)1020 private Element findParentConfigByUniqueRuleId(Element childElement) { 1021 NamedNodeMap attributes = childElement.getAttributes(); 1022 for (int i = 0; i < attributes.getLength(); i++) { 1023 String attribute = attributes.item(i).getNodeName(); 1024 String value = attributes.item(i).getNodeValue(); 1025 switch (attribute) { 1026 case "following": 1027 return mUniqueRules.get(value); 1028 //break; 1029 default: 1030 break; 1031 } 1032 } 1033 return null; 1034 } 1035 1036 } 1037