package com.android.carrierconfig; import android.Manifest; import android.annotation.NonNull; import android.content.Context; import android.content.res.AssetManager; import android.content.res.Resources; import android.database.Cursor; import android.os.PersistableBundle; import android.provider.Telephony; import android.service.carrier.CarrierIdentifier; import android.telephony.CarrierConfigManager; import android.telephony.TelephonyManager; import android.test.InstrumentationTestCase; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import junit.framework.AssertionFailedError; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; public class CarrierConfigTest extends InstrumentationTestCase { private static final String TAG = "CarrierConfigTest"; /** * Iterate over all XML files in assets/ and ensure they parse without error. */ public void testAllFilesParse() { forEachConfigXml(new ParserChecker() { public void check(XmlPullParser parser, String mccmnc) throws XmlPullParserException, IOException { PersistableBundle b = DefaultCarrierConfigService.readConfigFromXml(parser, new CarrierIdentifier("001", "001", "Test", "001001123456789", "", "")); assertNotNull("got null bundle", b); } }); } /** * Check that the config bundles in XML files have valid filter attributes. * This checks the attribute names only. */ public void testFilterValidAttributes() { forEachConfigXml(new ParserChecker() { public void check(XmlPullParser parser, String mccmnc) throws XmlPullParserException, IOException { int event; while (((event = parser.next()) != XmlPullParser.END_DOCUMENT)) { if (event == XmlPullParser.START_TAG && "carrier_config".equals(parser.getName())) { for (int i = 0; i < parser.getAttributeCount(); ++i) { String attribute = parser.getAttributeName(i); switch (attribute) { case "mcc": case "mnc": case "gid1": case "gid2": case "spn": case "imsi": case "device": case "cid": case "name": break; default: fail("Unknown attribute '" + attribute + "' at " + parser.getPositionDescription()); break; } } } } } }); } /** * Check that XML files named after mccmnc are those without matching carrier id. * If there is a matching carrier id, all configurations should move to carrierid.xml which * has a higher matching priority than mccmnc.xml */ public void testCarrierConfigFileNaming() { forEachConfigXml(new ParserChecker() { public void check(XmlPullParser parser, String mccmnc) throws XmlPullParserException, IOException { if (mccmnc == null) { // only check file named after mccmnc return; } int event; while (((event = parser.next()) != XmlPullParser.END_DOCUMENT)) { if (event == XmlPullParser.START_TAG && "carrier_config".equals(parser.getName())) { String mcc = null; String mnc = null; String spn = null; String gid1 = null; String gid2 = null; String imsi = null; for (int i = 0; i < parser.getAttributeCount(); ++i) { String attribute = parser.getAttributeName(i); switch (attribute) { case "mcc": mcc = parser.getAttributeValue(i); break; case "mnc": mnc = parser.getAttributeValue(i); break; case "gid1": gid1 = parser.getAttributeValue(i); break; case "gid2": gid2 = parser.getAttributeValue(i); break; case "spn": spn = parser.getAttributeValue(i); break; case "imsi": imsi = parser.getAttributeValue(i); break; default: fail("Unknown attribute '" + attribute + "' at " + parser.getPositionDescription()); break; } } mcc = (mcc != null) ? mcc : mccmnc.substring(0, 3); mnc = (mnc != null) ? mnc : mccmnc.substring(3); // check if there is a valid carrier id int carrierId = getCarrierId(getInstrumentation().getTargetContext(), new CarrierIdentifier(mcc, mnc, spn, imsi, gid1, gid2)); if (carrierId != TelephonyManager.UNKNOWN_CARRIER_ID) { fail("unexpected carrier_config_mccmnc.xml with matching carrier id: " + carrierId + ", please move to carrier_config_carrierid.xml"); } } } } }); } /** * Tests that the variable names in each XML file match actual keys in CarrierConfigManager. */ public void testVariableNames() { final Set varXmlNames = getCarrierConfigXmlNames(); // organize them into sets by type or unknown forEachConfigXml(new ParserChecker() { public void check(XmlPullParser parser, String mccmnc) throws XmlPullParserException, IOException { int event; while (((event = parser.next()) != XmlPullParser.END_DOCUMENT)) { if (event == XmlPullParser.START_TAG) { switch (parser.getName()) { case "int-array": case "string-array": // string-array and int-array require the 'num' attribute final String varNum = parser.getAttributeValue(null, "num"); assertNotNull("No 'num' attribute in array: " + parser.getPositionDescription(), varNum); case "int": case "long": case "boolean": case "string": // NOTE: This doesn't check for other valid Bundle values, but it // is limited to the key types in CarrierConfigManager. final String varName = parser.getAttributeValue(null, "name"); assertNotNull("No 'name' attribute: " + parser.getPositionDescription(), varName); assertTrue("Unknown variable: '" + varName + "' at " + parser.getPositionDescription(), varXmlNames.contains(varName)); // TODO: Check that the type is correct. break; case "carrier_config_list": case "item": case "carrier_config": // do nothing break; default: fail("unexpected tag: '" + parser.getName() + "' at " + parser.getPositionDescription()); break; } } } } }); } /** * Utility for iterating over each XML document in the assets folder. * * This can be used with {@link #forEachConfigXml} to run checks on each XML document. * {@link #check} should {@link #fail} if the test does not pass. */ private interface ParserChecker { void check(XmlPullParser parser, String mccmnc) throws XmlPullParserException, IOException; } /** * Utility for iterating over each XML document in the assets folder. */ private void forEachConfigXml(ParserChecker checker) { AssetManager assetMgr = getInstrumentation().getTargetContext().getAssets(); String mccmnc = null; try { String[] files = assetMgr.list(""); assertNotNull("failed to list files", files); assertTrue("no files", files.length > 0); for (String fileName : files) { try { if (!fileName.startsWith("carrier_config_")) continue; if (fileName.startsWith("carrier_config_mccmnc_")) { mccmnc = fileName.substring("carrier_config_mccmnc_".length(), fileName.indexOf(".xml")); } XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser parser = factory.newPullParser(); parser.setInput(assetMgr.open(fileName), "utf-8"); checker.check(parser, mccmnc); } catch (Throwable e) { throw new AssertionError("Problem in " + fileName + ": " + e.getMessage(), e); } } // Check vendor.xml too try { Resources res = getInstrumentation().getTargetContext().getResources(); checker.check(res.getXml(R.xml.vendor), mccmnc); } catch (Throwable e) { throw new AssertionError("Problem in vendor.xml: " + e.getMessage(), e); } } catch (IOException e) { fail(e.toString()); } } /** * Get the set of config variable names, as used in XML files. */ private Set getCarrierConfigXmlNames() { Set names = new HashSet<>(); // get values of all KEY_ members of CarrierConfigManager as well as its nested classes. names.addAll(getCarrierConfigXmlNames(CarrierConfigManager.class)); for (Class nested : CarrierConfigManager.class.getDeclaredClasses()) { Log.i("CarrierConfigTest", nested.toString()); if (Modifier.isStatic(nested.getModifiers())) { names.addAll(getCarrierConfigXmlNames(nested)); } } return names; } private Set getCarrierConfigXmlNames(Class clazz) { // get values of all KEY_ members of clazz Field[] fields = clazz.getDeclaredFields(); HashSet varXmlNames = new HashSet<>(); for (Field f : fields) { if (!f.getName().startsWith("KEY_")) continue; if (!Modifier.isStatic(f.getModifiers())) { fail("non-static key in " + clazz.getName() + ":" + f.toString()); } try { String value = (String) f.get(null); varXmlNames.add(value); } catch (IllegalAccessException e) { throw new AssertionError("Failed to get config key: " + e.getMessage(), e); } } assertTrue("Found zero keys", varXmlNames.size() > 0); return varXmlNames; } // helper function to get carrier id from carrierIdentifier private int getCarrierId(@NonNull Context context, @NonNull CarrierIdentifier carrierIdentifier) { try { getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( Manifest.permission.READ_PRIVILEGED_PHONE_STATE); List args = new ArrayList<>(); args.add(carrierIdentifier.getMcc() + carrierIdentifier.getMnc()); if (carrierIdentifier.getGid1() != null) { args.add(carrierIdentifier.getGid1()); } if (carrierIdentifier.getGid2() != null) { args.add(carrierIdentifier.getGid2()); } if (carrierIdentifier.getImsi() != null) { args.add(carrierIdentifier.getImsi()); } if (carrierIdentifier.getSpn() != null) { args.add(carrierIdentifier.getSpn()); } try (Cursor cursor = context.getContentResolver().query( Telephony.CarrierId.All.CONTENT_URI, /* projection */ null, /* selection */ Telephony.CarrierId.All.MCCMNC + "=? AND " + Telephony.CarrierId.All.GID1 + ((carrierIdentifier.getGid1() == null) ? " is NULL" : "=?") + " AND " + Telephony.CarrierId.All.GID2 + ((carrierIdentifier.getGid2() == null) ? " is NULL" : "=?") + " AND " + Telephony.CarrierId.All.IMSI_PREFIX_XPATTERN + ((carrierIdentifier.getImsi() == null) ? " is NULL" : "=?") + " AND " + Telephony.CarrierId.All.SPN + ((carrierIdentifier.getSpn() == null) ? " is NULL" : "=?"), /* selectionArgs */ args.toArray(new String[args.size()]), null)) { if (cursor != null) { while (cursor.moveToNext()) { return cursor.getInt(cursor.getColumnIndex(Telephony.CarrierId.CARRIER_ID)); } } } } catch (SecurityException e) { fail("Should be able to access APIs protected by a permission apps cannot get"); } finally { getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); } return TelephonyManager.UNKNOWN_CARRIER_ID; } }