1 /*
2  * Copyright (C) 2020 The Android Open Source Project
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 
17 package com.android.carrierconfig;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.os.Build;
22 import android.os.PersistableBundle;
23 import android.os.SystemProperties;
24 import android.service.carrier.CarrierIdentifier;
25 import android.service.carrier.CarrierService;
26 import android.telephony.TelephonyManager;
27 import android.text.TextUtils;
28 import android.util.Log;
29 
30 import org.xmlpull.v1.XmlPullParser;
31 import org.xmlpull.v1.XmlPullParserException;
32 import org.xmlpull.v1.XmlPullParserFactory;
33 
34 import java.io.IOException;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37 
38 /**
39  * Provides network overrides for carrier configuration.
40  *
41  * The configuration available through CarrierConfigManager is a combination of default values,
42  * default network overrides, and carrier overrides. The default network overrides are provided by
43  * this service. For a given network, we look for a matching XML file in our assets folder, and
44  * return the PersistableBundle from that file. Assets are preferred over Resources because resource
45  * overlays only support using MCC+MNC and that doesn't work with MVNOs. The only resource file used
46  * is vendor.xml, to provide vendor-specific overrides.
47  */
48 public class DefaultCarrierConfigService extends CarrierService {
49 
50     private static final String SPN_EMPTY_MATCH = "null";
51 
52     private static final String CARRIER_ID_PREFIX = "carrier_config_carrierid_";
53 
54     private static final String MCCMNC_PREFIX = "carrier_config_mccmnc_";
55 
56     private static final String NO_SIM_CONFIG_FILE_NAME = "carrier_config_no_sim.xml";
57 
58     private static final String TAG = "DefaultCarrierConfigService";
59 
60     private XmlPullParserFactory mFactory;
61 
DefaultCarrierConfigService()62     public DefaultCarrierConfigService() {
63         Log.d(TAG, "Service created");
64         mFactory = null;
65     }
66 
67     /**
68      * Returns per-network overrides for carrier configuration.
69      *
70      * This returns a carrier config bundle appropriate for the given carrier by reading data from
71      * files in our assets folder. Config files in assets folder are carrier-id-indexed
72      * {@link TelephonyManager#getSimCarrierId()}. NOTE: config files named after mccmnc
73      * are for those without a matching carrier id and should be renamed to carrier id once the
74      * missing IDs are added to
75      * <a href="https://android.googlesource.com/platform/packages/providers/TelephonyProvider/+/master/assets/latest_carrier_id/carrier_list.textpb">carrier id list</a>
76      *
77      * First, look for file named after
78      * carrier_config_carrierid_<carrierid>_<carriername>.xml if carrier id is not
79      * {@link TelephonyManager#UNKNOWN_CARRIER_ID}. Note <carriername> is to improve the
80      * readability which should not be used to search asset files. If there is no configuration,
81      * then we look for a file named after the MCC+MNC of {@code id} as a fallback. Last, we read
82      * res/xml/vendor.xml.
83      *
84      * carrierid.xml doesn't support multiple bundles with filters as each carrier including MVNOs
85      * has its own config file named after its carrier id.
86      * Both vendor.xml and MCC+MNC.xml files may contain multiple bundles with filters on them.
87      * All the matching bundles are flattened to return one carrier config bundle.
88      */
89     @Override
onLoadConfig(@ullable CarrierIdentifier id)90     public PersistableBundle onLoadConfig(@Nullable CarrierIdentifier id) {
91         Log.d(TAG, "Config being fetched");
92 
93         try {
94             synchronized (this) {
95                 if (mFactory == null) {
96                     mFactory = XmlPullParserFactory.newInstance();
97                 }
98             }
99 
100             XmlPullParser parser = mFactory.newPullParser();
101 
102             return loadConfig(parser, id);
103         }
104         catch (XmlPullParserException e) {
105             Log.e(TAG, "Failed to load config", e);
106             return new PersistableBundle();
107         }
108     }
109 
loadConfig(XmlPullParser parser, @Nullable CarrierIdentifier id)110     PersistableBundle loadConfig(XmlPullParser parser, @Nullable CarrierIdentifier id) {
111         PersistableBundle config = new PersistableBundle();
112         // OEM customizable filter for carrier requirements not related to hardware/vendor SKU.
113         String sku = getApplicationContext().getResources().getString(R.string.sku_filter);
114 
115         if (id == null) {
116             try {
117                 // Load no SIM config if carrier id is not set.
118                 parser.setInput(getApplicationContext().getAssets().open(
119                         NO_SIM_CONFIG_FILE_NAME), "utf-8");
120                 config = readConfigFromXml(parser, null, sku);
121 
122                 // Treat vendor_no_sim.xml as if it were appended to the no sim config file.
123                 XmlPullParser vendorInput =
124                         getApplicationContext().getResources().getXml(R.xml.vendor_no_sim);
125                 PersistableBundle vendorConfig = readConfigFromXml(vendorInput, null, sku);
126                 config.putAll(vendorConfig);
127             }
128             catch (IOException|XmlPullParserException e) {
129                 Log.e(TAG, "Failed to load config for no SIM", e);
130             }
131 
132             return config;
133         }
134 
135         try {
136             if (id.getCarrierId() != TelephonyManager.UNKNOWN_CARRIER_ID) {
137                 PersistableBundle configByCarrierId = new PersistableBundle();
138                 PersistableBundle configBySpecificCarrierId = new PersistableBundle();
139                 PersistableBundle configByMccMncFallBackCarrierId = new PersistableBundle();
140                 TelephonyManager telephonyManager = getApplicationContext()
141                         .getSystemService(TelephonyManager.class);
142                 int mccmncCarrierId = telephonyManager
143                         .getCarrierIdFromMccMnc(id.getMcc() + id.getMnc());
144                 for (String file : getApplicationContext().getAssets().list("")) {
145                     if (file.startsWith(CARRIER_ID_PREFIX + id.getSpecificCarrierId() + "_")) {
146                         parser.setInput(getApplicationContext().getAssets().open(file), "utf-8");
147                         configBySpecificCarrierId = readConfigFromXml(parser, null, sku);
148                         break;
149                     } else if (file.startsWith(CARRIER_ID_PREFIX + id.getCarrierId() + "_")) {
150                         parser.setInput(getApplicationContext().getAssets().open(file), "utf-8");
151                         configByCarrierId = readConfigFromXml(parser, null, sku);
152                     } else if (file.startsWith(CARRIER_ID_PREFIX + mccmncCarrierId + "_")) {
153                         parser.setInput(getApplicationContext().getAssets().open(file), "utf-8");
154                         configByMccMncFallBackCarrierId = readConfigFromXml(parser, null, sku);
155                     }
156                 }
157 
158                 // priority: specific carrier id > carrier id > mccmnc fallback carrier id
159                 if (!configBySpecificCarrierId.isEmpty()) {
160                     config = configBySpecificCarrierId;
161                 } else if (!configByCarrierId.isEmpty()) {
162                     config = configByCarrierId;
163                 } else if (!configByMccMncFallBackCarrierId.isEmpty()) {
164                     config = configByMccMncFallBackCarrierId;
165                 }
166             }
167             if (config.isEmpty()) {
168                 // fallback to use mccmnc.xml when there is no carrier id named config found.
169                 parser.setInput(getApplicationContext().getAssets().open(
170                         MCCMNC_PREFIX + id.getMcc() + id.getMnc() + ".xml"), "utf-8");
171                 config = readConfigFromXml(parser, id, sku);
172             }
173         }
174         catch (IOException | XmlPullParserException e) {
175             Log.d(TAG, e.toString());
176             // We can return an empty config for unknown networks.
177             config = new PersistableBundle();
178         }
179 
180         // Treat vendor.xml as if it were appended to the carrier config file we read.
181         XmlPullParser vendorInput = getApplicationContext().getResources().getXml(R.xml.vendor);
182         try {
183             PersistableBundle vendorConfig = readConfigFromXml(vendorInput, id, sku);
184             config.putAll(vendorConfig);
185         }
186         catch (IOException | XmlPullParserException e) {
187             Log.e(TAG, e.toString());
188         }
189 
190         return config;
191     }
192 
193     /**
194      * Parses an XML document and returns a PersistableBundle.
195      *
196      * <p>This function iterates over each {@code <carrier_config>} node in the XML document and
197      * parses it into a bundle if its filters match {@code id}. XML documents named after carrier id
198      * doesn't support filter match as each carrier including MVNOs will have its own config file.
199      * The format of XML bundles is defined
200      * by {@link PersistableBundle#restoreFromXml}. All the matching bundles will be flattened and
201      * returned as a single bundle.</p>
202      *
203      * <p>Here is an example document in vendor.xml.
204      * <pre>{@code
205      * <carrier_config_list>
206      *     <carrier_config cid="1938" name="verizon">
207      *         <boolean name="voicemail_notification_persistent_bool" value="true" />
208      *     </carrier_config>
209      *     <carrier_config cid="1788" name="sprint">
210      *         <boolean name="voicemail_notification_persistent_bool" value="false" />
211      *     </carrier_config>
212      * </carrier_config_list>
213      * }</pre></p>
214      *
215      * <p>Here is an example document. The second bundle will be applied to the first only if the
216      * GID1 is ABCD.
217      * <pre>{@code
218      * <carrier_config_list>
219      *     <carrier_config>
220      *         <boolean name="voicemail_notification_persistent_bool" value="true" />
221      *     </carrier_config>
222      *     <carrier_config gid1="ABCD">
223      *         <boolean name="voicemail_notification_persistent_bool" value="false" />
224      *     </carrier_config>
225      * </carrier_config_list>
226      * }</pre></p>
227      *
228      * @param parser an XmlPullParser pointing at the beginning of the document.
229      * @param id the details of the SIM operator used to filter parts of the document. If read from
230      *           files named after carrier id, this will be set to {@null code} as no filter match
231      *           needed.
232      * @param sku a filter to be customizable.
233      * @return a possibly empty PersistableBundle containing the config values.
234      */
readConfigFromXml(XmlPullParser parser, @Nullable CarrierIdentifier id, String sku)235     static PersistableBundle readConfigFromXml(XmlPullParser parser, @Nullable CarrierIdentifier id,
236             String sku) throws IOException, XmlPullParserException {
237         PersistableBundle config = new PersistableBundle();
238 
239         if (parser == null) {
240           return config;
241         }
242 
243         // Iterate over each <carrier_config> node in the document and add it to the returned
244         // bundle if its filters match.
245         int event;
246         while (((event = parser.next()) != XmlPullParser.END_DOCUMENT)) {
247             if (event == XmlPullParser.START_TAG && "carrier_config".equals(parser.getName())) {
248                 // Skip this fragment if it has filters that don't match.
249                 if (!checkFilters(parser, id, sku)) {
250                     continue;
251                 }
252                 PersistableBundle configFragment = PersistableBundle.restoreFromXml(parser);
253                 config.putAll(configFragment);
254             }
255         }
256 
257         return config;
258     }
259 
260     /**
261      * Checks to see if an XML node matches carrier filters.
262      *
263      * <p>This iterates over the attributes of the current tag pointed to by {@code parser} and
264      * checks each one against {@code id} or {@link Build.DEVICE} or {@link R.string#sku_filter}.
265      * Attributes that are not specified in the node will not be checked, so a node with no
266      * attributes will always return true. The supported filter attributes are,
267      * <ul>
268      *   <li>mcc: {@link CarrierIdentifier#getMcc}</li>
269      *   <li>mnc: {@link CarrierIdentifier#getMnc}</li>
270      *   <li>gid1: {@link CarrierIdentifier#getGid1}</li>
271      *   <li>gid2: {@link CarrierIdentifier#getGid2}</li>
272      *   <li>spn: {@link CarrierIdentifier#getSpn}</li>
273      *   <li>imsi: {@link CarrierIdentifier#getImsi}</li>
274      *   <li>device: {@link Build.DEVICE}</li>
275      *   <li>vendorSku: {@link SystemConfig.VENDOR_SKU_PROPERTY}</li>
276      *   <li>hardwareSku: {@link SystemConfig.SKU_PROPERTY}</li>
277      *   <li>cid: {@link CarrierIdentifier#getCarrierId()}
278      *   or {@link CarrierIdentifier#getSpecificCarrierId()}</li>
279      *   <li>sku: {@link R.string#sku_filter} "sku_filter" that OEM customizable filter</li>
280      * </ul>
281      * </p>
282      *
283      * <p>
284      * The attributes imsi and spn can be expressed as regexp to filter on patterns.
285      * The spn attribute can be set to the string "null" to allow matching against a SIM
286      * with no spn set.
287      * </p>
288      *
289      * @param parser an XmlPullParser pointing at a START_TAG with the attributes to check.
290      * @param id the carrier details to check against.
291      * @param sku a filter to be customizable.
292      * @return false if any XML attribute does not match the corresponding value.
293      */
checkFilters(XmlPullParser parser, @Nullable CarrierIdentifier id, String sku)294     static boolean checkFilters(XmlPullParser parser, @Nullable CarrierIdentifier id, String sku) {
295         String vendorSkuProperty = SystemProperties.get(
296             "ro.boot.product.vendor.sku", "");
297         String hardwareSkuProperty = SystemProperties.get(
298             "ro.boot.product.hardware.sku", "");
299         for (int i = 0; i < parser.getAttributeCount(); ++i) {
300             boolean result = true;
301             String attribute = parser.getAttributeName(i);
302             String value = parser.getAttributeValue(i);
303             switch (attribute) {
304                 case "mcc":
305                     result = (id == null) || value.equals(id.getMcc());
306                     break;
307                 case "mnc":
308                     result = (id == null) || value.equals(id.getMnc());
309                     break;
310                 case "gid1":
311                     result = (id == null) || value.equalsIgnoreCase(id.getGid1());
312                     break;
313                 case "gid2":
314                     result = (id == null) || value.equalsIgnoreCase(id.getGid2());
315                     break;
316                 case "spn":
317                     result = (id == null) || matchOnSP(value, id);
318                     break;
319                 case "imsi":
320                     result = (id == null) || matchOnImsi(value, id);
321                     break;
322                 case "device":
323                     result = value.equalsIgnoreCase(Build.DEVICE);
324                     break;
325                 case "vendorSku":
326                     result = value.equalsIgnoreCase(vendorSkuProperty);
327                     break;
328                 case "hardwareSku":
329                     result = value.equalsIgnoreCase(hardwareSkuProperty);
330                     break;
331                 case "cid":
332                     result = (id == null) || (Integer.parseInt(value) == id.getCarrierId())
333                                 || (Integer.parseInt(value) == id.getSpecificCarrierId());
334                     break;
335                 case "name":
336                     // name is used together with cid for readability. ignore for filter.
337                     break;
338                 case "sku":
339                     result = value.equalsIgnoreCase(sku);
340                     break;
341                 default:
342                     Log.e(TAG, "Unknown attribute " + attribute + "=" + value);
343                     result = false;
344                     break;
345             }
346 
347             if (!result) {
348                 return false;
349             }
350         }
351         return true;
352     }
353 
354     /**
355      * Check to see if the IMSI expression from the XML matches the IMSI of the
356      * Carrier.
357      *
358      * @param xmlImsi IMSI expression fetched from the resource XML
359      * @param id Id of the evaluated CarrierIdentifier
360      * @return true if the XML IMSI matches the IMSI of CarrierIdentifier, false
361      *         otherwise.
362      */
matchOnImsi(String xmlImsi, CarrierIdentifier id)363     static boolean matchOnImsi(String xmlImsi, CarrierIdentifier id) {
364         boolean matchFound = false;
365 
366         String currentImsi = id.getImsi();
367         // If we were able to retrieve current IMSI, see if it matches.
368         if (currentImsi != null) {
369             Pattern imsiPattern = Pattern.compile(xmlImsi, Pattern.CASE_INSENSITIVE);
370             Matcher matcher = imsiPattern.matcher(currentImsi);
371             matchFound = matcher.matches();
372         }
373         return matchFound;
374     }
375 
376     /**
377      * Check to see if the service provider name expression from the XML matches the
378      * CarrierIdentifier.
379      *
380      * @param xmlSP SP expression fetched from the resource XML
381      * @param id Id of the evaluated CarrierIdentifier
382      * @return true if the XML SP matches the phone's SP, false otherwise.
383      */
matchOnSP(String xmlSP, CarrierIdentifier id)384     static boolean matchOnSP(String xmlSP, CarrierIdentifier id) {
385         boolean matchFound = false;
386 
387         String currentSP = id.getSpn();
388         if (SPN_EMPTY_MATCH.equalsIgnoreCase(xmlSP)) {
389             if (TextUtils.isEmpty(currentSP)) {
390                 matchFound = true;
391             }
392         } else if (currentSP != null) {
393             Pattern spPattern = Pattern.compile(xmlSP, Pattern.CASE_INSENSITIVE);
394             Matcher matcher = spPattern.matcher(currentSP);
395             matchFound = matcher.matches();
396         }
397         return matchFound;
398     }
399 }
400