/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.mms; import android.content.ContentValues; import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.Telephony; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import com.android.messaging.R; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; /** * Default implementation of APN settings loader */ class DefaultApnSettingsLoader implements ApnSettingsLoader { /** * The base implementation of an APN */ private static class BaseApn implements Apn { /** * Create a base APN from parameters * * @param typesIn the APN type field * @param mmscIn the APN mmsc field * @param proxyIn the APN mmsproxy field * @param portIn the APN mmsport field * @return an instance of base APN, or null if any of the parameter is invalid */ public static BaseApn from(final String typesIn, final String mmscIn, final String proxyIn, final String portIn) { if (!isValidApnType(trimWithNullCheck(typesIn), APN_TYPE_MMS)) { return null; } String mmsc = trimWithNullCheck(mmscIn); if (TextUtils.isEmpty(mmsc)) { return null; } mmsc = trimV4AddrZeros(mmsc); try { new URI(mmsc); } catch (final URISyntaxException e) { return null; } String mmsProxy = trimWithNullCheck(proxyIn); int mmsProxyPort = 80; if (!TextUtils.isEmpty(mmsProxy)) { mmsProxy = trimV4AddrZeros(mmsProxy); final String portString = trimWithNullCheck(portIn); if (portString != null) { try { mmsProxyPort = Integer.parseInt(portString); } catch (final NumberFormatException e) { // Ignore, just use 80 to try } } } return new BaseApn(mmsc, mmsProxy, mmsProxyPort); } private final String mMmsc; private final String mMmsProxy; private final int mMmsProxyPort; public BaseApn(final String mmsc, final String proxy, final int port) { mMmsc = mmsc; mMmsProxy = proxy; mMmsProxyPort = port; } @Override public String getMmsc() { return mMmsc; } @Override public String getMmsProxy() { return mMmsProxy; } @Override public int getMmsProxyPort() { return mMmsProxyPort; } @Override public void setSuccess() { // Do nothing } public boolean equals(final BaseApn other) { return TextUtils.equals(mMmsc, other.getMmsc()) && TextUtils.equals(mMmsProxy, other.getMmsProxy()) && mMmsProxyPort == other.getMmsProxyPort(); } } /** * An in-memory implementation of an APN. These APNs are organized into an in-memory list. * The order of the list can be changed by the setSuccess method. */ private static class MemoryApn implements Apn { /** * Create an in-memory APN loaded from resources * * @param apns the in-memory APN list * @param typesIn the APN type field * @param mmscIn the APN mmsc field * @param proxyIn the APN mmsproxy field * @param portIn the APN mmsport field * @return an in-memory APN instance, null if there is invalid parameter */ public static MemoryApn from(final List apns, final String typesIn, final String mmscIn, final String proxyIn, final String portIn) { if (apns == null) { return null; } final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn); if (base == null) { return null; } for (final Apn apn : apns) { if (apn instanceof MemoryApn && ((MemoryApn) apn).equals(base)) { return null; } } return new MemoryApn(apns, base); } private final List mApns; private final BaseApn mBase; public MemoryApn(final List apns, final BaseApn base) { mApns = apns; mBase = base; } @Override public String getMmsc() { return mBase.getMmsc(); } @Override public String getMmsProxy() { return mBase.getMmsProxy(); } @Override public int getMmsProxyPort() { return mBase.getMmsProxyPort(); } @Override public void setSuccess() { // If this is being marked as a successful APN, move it to the top of the list so // next time it will be tried first boolean moved = false; synchronized (mApns) { if (mApns.get(0) != this) { mApns.remove(this); mApns.add(0, this); moved = true; } } if (moved) { Log.d(MmsService.TAG, "Set APN [" + "MMSC=" + getMmsc() + ", " + "PROXY=" + getMmsProxy() + ", " + "PORT=" + getMmsProxyPort() + "] to be first"); } } public boolean equals(final BaseApn other) { if (other == null) { return false; } return mBase.equals(other); } } /** * APN_TYPE_ALL is a special type to indicate that this APN entry can * service all data connections. */ public static final String APN_TYPE_ALL = "*"; /** APN type for MMS traffic */ public static final String APN_TYPE_MMS = "mms"; private static final String[] APN_PROJECTION = { Telephony.Carriers.TYPE, Telephony.Carriers.MMSC, Telephony.Carriers.MMSPROXY, Telephony.Carriers.MMSPORT, }; private static final int COLUMN_TYPE = 0; private static final int COLUMN_MMSC = 1; private static final int COLUMN_MMSPROXY = 2; private static final int COLUMN_MMSPORT = 3; private static final String APN_MCC = "mcc"; private static final String APN_MNC = "mnc"; private static final String APN_APN = "apn"; private static final String APN_TYPE = "type"; private static final String APN_MMSC = "mmsc"; private static final String APN_MMSPROXY = "mmsproxy"; private static final String APN_MMSPORT = "mmsport"; private final Context mContext; // Cached APNs for subIds private final SparseArray> mApnsCache; DefaultApnSettingsLoader(final Context context) { mContext = context; mApnsCache = new SparseArray<>(); } @Override public List get(final String apnName) { final int subId = Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID); List apns; boolean didLoad = false; synchronized (this) { apns = mApnsCache.get(subId); if (apns == null) { apns = new ArrayList<>(); mApnsCache.put(subId, apns); loadLocked(subId, apnName, apns); didLoad = true; } } if (didLoad) { Log.i(MmsService.TAG, "Loaded " + apns.size() + " APNs"); } return apns; } private void loadLocked(final int subId, final String apnName, final List apns) { // Try system APN table first loadFromSystem(subId, apnName, apns); if (apns.size() > 0) { return; } // Try loading from apns.xml in resources loadFromResources(subId, apnName, apns); if (apns.size() > 0) { return; } // Try resources but without APN name loadFromResources(subId, null/*apnName*/, apns); } /** * Load matching APNs from telephony provider. * We try different combinations of the query to work around some platform quirks. * * @param subId the SIM subId * @param apnName the APN name to match * @param apns the list used to return results */ private void loadFromSystem(final int subId, final String apnName, final List apns) { Uri uri; if (Utils.supportMSim() && subId != MmsManager.DEFAULT_SUB_ID) { uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "/subId/" + subId); } else { uri = Telephony.Carriers.CONTENT_URI; } Cursor cursor = null; try { for (; ; ) { // Try different combinations of queries. Some would work on some platforms. // So we query each combination until we find one returns non-empty result. cursor = querySystem(uri, true/*checkCurrent*/, apnName); if (cursor != null) { break; } cursor = querySystem(uri, false/*checkCurrent*/, apnName); if (cursor != null) { break; } cursor = querySystem(uri, true/*checkCurrent*/, null/*apnName*/); if (cursor != null) { break; } cursor = querySystem(uri, false/*checkCurrent*/, null/*apnName*/); break; } } catch (final SecurityException e) { // Can't access platform APN table, return directly return; } if (cursor == null) { return; } try { if (cursor.moveToFirst()) { final Apn apn = BaseApn.from( cursor.getString(COLUMN_TYPE), cursor.getString(COLUMN_MMSC), cursor.getString(COLUMN_MMSPROXY), cursor.getString(COLUMN_MMSPORT)); if (apn != null) { apns.add(apn); } } } finally { cursor.close(); } } /** * Query system APN table * * @param uri The APN query URL to use * @param checkCurrent If add "CURRENT IS NOT NULL" condition * @param apnName The optional APN name for query condition * @return A cursor of the query result. If a cursor is returned as not null, it is * guaranteed to contain at least one row. */ private Cursor querySystem(final Uri uri, final boolean checkCurrent, String apnName) { Log.i(MmsService.TAG, "Loading APNs from system, " + "checkCurrent=" + checkCurrent + " apnName=" + apnName); final StringBuilder selectionBuilder = new StringBuilder(); String[] selectionArgs = null; if (checkCurrent) { selectionBuilder.append(Telephony.Carriers.CURRENT).append(" IS NOT NULL"); } apnName = trimWithNullCheck(apnName); if (!TextUtils.isEmpty(apnName)) { if (selectionBuilder.length() > 0) { selectionBuilder.append(" AND "); } selectionBuilder.append(Telephony.Carriers.APN).append("=?"); selectionArgs = new String[] { apnName }; } try { final Cursor cursor = mContext.getContentResolver().query( uri, APN_PROJECTION, selectionBuilder.toString(), selectionArgs, null/*sortOrder*/); if (cursor == null || cursor.getCount() < 1) { if (cursor != null) { cursor.close(); } Log.w(MmsService.TAG, "Query " + uri + " with apn " + apnName + " and " + (checkCurrent ? "checking CURRENT" : "not checking CURRENT") + " returned empty"); return null; } return cursor; } catch (final SQLiteException e) { Log.w(MmsService.TAG, "APN table query exception: " + e); } catch (final SecurityException e) { Log.w(MmsService.TAG, "Platform restricts APN table access: " + e); throw e; } return null; } /** * Find matching APNs using builtin APN list resource * * @param subId the SIM subId * @param apnName the APN name to match * @param apns the list for returning results */ private void loadFromResources(final int subId, final String apnName, final List apns) { Log.i(MmsService.TAG, "Loading APNs from resources, apnName=" + apnName); final int[] mccMnc = Utils.getMccMnc(mContext, subId); if (mccMnc[0] == 0 && mccMnc[0] == 0) { Log.w(MmsService.TAG, "Can not get valid mcc/mnc from system"); return; } // MCC/MNC is good, loading/querying APNs from XML XmlResourceParser xml = null; try { xml = mContext.getResources().getXml(R.xml.apns); new ApnsXmlParser(xml, new ApnsXmlParser.ApnProcessor() { @Override public void process(ContentValues apnValues) { final String mcc = trimWithNullCheck(apnValues.getAsString(APN_MCC)); final String mnc = trimWithNullCheck(apnValues.getAsString(APN_MNC)); final String apn = trimWithNullCheck(apnValues.getAsString(APN_APN)); try { if (mccMnc[0] == Integer.parseInt(mcc) && mccMnc[1] == Integer.parseInt(mnc) && (TextUtils.isEmpty(apnName) || apnName.equalsIgnoreCase(apn))) { final String type = apnValues.getAsString(APN_TYPE); final String mmsc = apnValues.getAsString(APN_MMSC); final String mmsproxy = apnValues.getAsString(APN_MMSPROXY); final String mmsport = apnValues.getAsString(APN_MMSPORT); final Apn newApn = MemoryApn.from(apns, type, mmsc, mmsproxy, mmsport); if (newApn != null) { apns.add(newApn); } } } catch (final NumberFormatException e) { // Ignore } } }).parse(); } catch (final Resources.NotFoundException e) { Log.w(MmsService.TAG, "Can not get apns.xml " + e); } finally { if (xml != null) { xml.close(); } } } private static String trimWithNullCheck(final String value) { return value != null ? value.trim() : null; } /** * Trim leading zeros from IPv4 address strings * Our base libraries will interpret that as octel.. * Must leave non v4 addresses and host names alone. * For example, 192.168.000.010 -> 192.168.0.10 * * @param addr a string representing an ip addr * @return a string propertly trimmed */ private static String trimV4AddrZeros(final String addr) { if (addr == null) { return null; } final String[] octets = addr.split("\\."); if (octets.length != 4) { return addr; } final StringBuilder builder = new StringBuilder(16); String result = null; for (int i = 0; i < 4; i++) { try { if (octets[i].length() > 3) { return addr; } builder.append(Integer.parseInt(octets[i])); } catch (final NumberFormatException e) { return addr; } if (i < 3) { builder.append('.'); } } result = builder.toString(); return result; } /** * Check if the APN contains the APN type we want * * @param types The string encodes a list of supported types * @param requestType The type we want * @return true if the input types string contains the requestType */ public static boolean isValidApnType(final String types, final String requestType) { // If APN type is unspecified, assume APN_TYPE_ALL. if (TextUtils.isEmpty(types)) { return true; } for (final String t : types.split(",")) { if (t.equals(requestType) || t.equals(APN_TYPE_ALL)) { return true; } } return false; } }