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