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