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