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