• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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