| /* |
| * 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 com.android.messaging.sms; |
| |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteException; |
| import android.net.Uri; |
| import android.provider.Telephony; |
| import androidx.appcompat.mms.ApnSettingsLoader; |
| import androidx.appcompat.mms.MmsManager; |
| import android.text.TextUtils; |
| import android.util.SparseArray; |
| |
| import com.android.messaging.datamodel.data.ParticipantData; |
| import com.android.messaging.mmslib.SqliteWrapper; |
| import com.android.messaging.util.BugleGservices; |
| import com.android.messaging.util.BugleGservicesKeys; |
| import com.android.messaging.util.LogUtil; |
| import com.android.messaging.util.OsUtil; |
| import com.android.messaging.util.PhoneUtils; |
| |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * APN loader for default SMS SIM |
| * |
| * This loader tries to load APNs from 3 sources in order: |
| * 1. Gservices setting |
| * 2. System APN table |
| * 3. Local APN table |
| */ |
| public class BugleApnSettingsLoader 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(); |
| } |
| } |
| |
| /** |
| * The APN represented by the local APN table row |
| */ |
| private static class DatabaseApn implements Apn { |
| private static final ContentValues CURRENT_NULL_VALUE; |
| private static final ContentValues CURRENT_SET_VALUE; |
| static { |
| CURRENT_NULL_VALUE = new ContentValues(1); |
| CURRENT_NULL_VALUE.putNull(Telephony.Carriers.CURRENT); |
| CURRENT_SET_VALUE = new ContentValues(1); |
| CURRENT_SET_VALUE.put(Telephony.Carriers.CURRENT, "1"); // 1 for auto selected APN |
| } |
| private static final String CLEAR_UPDATE_SELECTION = Telephony.Carriers.CURRENT + " =?"; |
| private static final String[] CLEAR_UPDATE_SELECTION_ARGS = new String[] { "1" }; |
| private static final String SET_UPDATE_SELECTION = Telephony.Carriers._ID + " =?"; |
| |
| /** |
| * Create an APN loaded from local database |
| * |
| * @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 |
| * @param rowId the APN's row ID in database |
| * @param current the value of CURRENT column in database |
| * @return an in-memory APN instance for database APN row, null if parameter invalid |
| */ |
| public static DatabaseApn from(final List<Apn> apns, final String typesIn, |
| final String mmscIn, final String proxyIn, final String portIn, |
| final long rowId, final int current) { |
| if (apns == null) { |
| return null; |
| } |
| final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn); |
| if (base == null) { |
| return null; |
| } |
| for (final ApnSettingsLoader.Apn apn : apns) { |
| if (apn instanceof DatabaseApn && ((DatabaseApn) apn).equals(base)) { |
| return null; |
| } |
| } |
| return new DatabaseApn(apns, base, rowId, current); |
| } |
| |
| private final List<Apn> mApns; |
| private final BaseApn mBase; |
| private final long mRowId; |
| private int mCurrent; |
| |
| public DatabaseApn(final List<Apn> apns, final BaseApn base, final long rowId, |
| final int current) { |
| mApns = apns; |
| mBase = base; |
| mRowId = rowId; |
| mCurrent = current; |
| } |
| |
| @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() { |
| moveToListHead(); |
| setCurrentInDatabase(); |
| } |
| |
| /** |
| * Try to move this APN to the head of in-memory list |
| */ |
| private void moveToListHead() { |
| // 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) { |
| LogUtil.d(LogUtil.BUGLE_TAG, "Set APN [" |
| + "MMSC=" + getMmsc() + ", " |
| + "PROXY=" + getMmsProxy() + ", " |
| + "PORT=" + getMmsProxyPort() + "] to be first"); |
| } |
| } |
| |
| /** |
| * Try to set the APN to be CURRENT in its database table |
| */ |
| private void setCurrentInDatabase() { |
| synchronized (this) { |
| if (mCurrent > 0) { |
| // Already current |
| return; |
| } |
| mCurrent = 1; |
| } |
| LogUtil.d(LogUtil.BUGLE_TAG, "Set APN @" + mRowId + " to be CURRENT in local db"); |
| final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase(); |
| database.beginTransaction(); |
| try { |
| // clear the previous current=1 apn |
| // we don't clear current=2 apn since it is manually selected by user |
| // and we should not override it. |
| database.update(ApnDatabase.APN_TABLE, CURRENT_NULL_VALUE, |
| CLEAR_UPDATE_SELECTION, CLEAR_UPDATE_SELECTION_ARGS); |
| // set this one to be current (1) |
| database.update(ApnDatabase.APN_TABLE, CURRENT_SET_VALUE, SET_UPDATE_SELECTION, |
| new String[] { Long.toString(mRowId) }); |
| database.setTransactionSuccessful(); |
| } finally { |
| database.endTransaction(); |
| } |
| } |
| |
| 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_SYSTEM = { |
| Telephony.Carriers.TYPE, |
| Telephony.Carriers.MMSC, |
| Telephony.Carriers.MMSPROXY, |
| Telephony.Carriers.MMSPORT, |
| }; |
| private static final String[] APN_PROJECTION_LOCAL = { |
| Telephony.Carriers.TYPE, |
| Telephony.Carriers.MMSC, |
| Telephony.Carriers.MMSPROXY, |
| Telephony.Carriers.MMSPORT, |
| Telephony.Carriers.CURRENT, |
| Telephony.Carriers._ID, |
| }; |
| 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 int COLUMN_CURRENT = 4; |
| private static final int COLUMN_ID = 5; |
| |
| private static final String SELECTION_APN = Telephony.Carriers.APN + "=?"; |
| private static final String SELECTION_CURRENT = Telephony.Carriers.CURRENT + " IS NOT NULL"; |
| private static final String SELECTION_NUMERIC = Telephony.Carriers.NUMERIC + "=?"; |
| private static final String ORDER_BY = Telephony.Carriers.CURRENT + " DESC"; |
| |
| private final Context mContext; |
| |
| // Cached APNs for subIds |
| private final SparseArray<List<ApnSettingsLoader.Apn>> mApnsCache; |
| |
| public BugleApnSettingsLoader(final Context context) { |
| mContext = context; |
| mApnsCache = new SparseArray<>(); |
| } |
| |
| @Override |
| public List<ApnSettingsLoader.Apn> get(final String apnName) { |
| final int subId = PhoneUtils.getDefault().getEffectiveSubId( |
| ParticipantData.DEFAULT_SELF_SUB_ID); |
| List<ApnSettingsLoader.Apn> 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) { |
| LogUtil.i(LogUtil.BUGLE_TAG, "Loaded " + apns.size() + " APNs"); |
| } |
| return apns; |
| } |
| |
| private void loadLocked(final int subId, final String apnName, final List<Apn> apns) { |
| // Try Gservices first |
| loadFromGservices(apns); |
| if (apns.size() > 0) { |
| return; |
| } |
| // Try system APN table |
| loadFromSystem(subId, apnName, apns); |
| if (apns.size() > 0) { |
| return; |
| } |
| // Try local APN table |
| loadFromLocalDatabase(apnName, apns); |
| if (apns.size() <= 0) { |
| LogUtil.w(LogUtil.BUGLE_TAG, "Failed to load any APN"); |
| } |
| } |
| |
| /** |
| * Load from Gservices if APN setting is set in Gservices |
| * |
| * @param apns the list used to return results |
| */ |
| private void loadFromGservices(final List<Apn> apns) { |
| final BugleGservices gservices = BugleGservices.get(); |
| final String mmsc = gservices.getString(BugleGservicesKeys.MMS_MMSC, null); |
| if (TextUtils.isEmpty(mmsc)) { |
| return; |
| } |
| LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from gservices"); |
| final String proxy = gservices.getString(BugleGservicesKeys.MMS_PROXY_ADDRESS, null); |
| final int port = gservices.getInt(BugleGservicesKeys.MMS_PROXY_PORT, -1); |
| final Apn apn = BaseApn.from("mms", mmsc, proxy, Integer.toString(port)); |
| if (apn != null) { |
| apns.add(apn); |
| } |
| } |
| |
| /** |
| * 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<Apn> apns) { |
| Uri uri; |
| if (OsUtil.isAtLeastL_MR1() && 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 ApnSettingsLoader.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) { |
| LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from system, " |
| + "checkCurrent=" + checkCurrent + " apnName=" + apnName); |
| final StringBuilder selectionBuilder = new StringBuilder(); |
| String[] selectionArgs = null; |
| if (checkCurrent) { |
| selectionBuilder.append(SELECTION_CURRENT); |
| } |
| apnName = trimWithNullCheck(apnName); |
| if (!TextUtils.isEmpty(apnName)) { |
| if (selectionBuilder.length() > 0) { |
| selectionBuilder.append(" AND "); |
| } |
| selectionBuilder.append(SELECTION_APN); |
| selectionArgs = new String[] { apnName }; |
| } |
| try { |
| final Cursor cursor = SqliteWrapper.query( |
| mContext, |
| mContext.getContentResolver(), |
| uri, |
| APN_PROJECTION_SYSTEM, |
| selectionBuilder.toString(), |
| selectionArgs, |
| null/*sortOrder*/); |
| if (cursor == null || cursor.getCount() < 1) { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| LogUtil.w(LogUtil.BUGLE_TAG, "Query " + uri + " with apn " + apnName + " and " |
| + (checkCurrent ? "checking CURRENT" : "not checking CURRENT") |
| + " returned empty"); |
| return null; |
| } |
| return cursor; |
| } catch (final SQLiteException e) { |
| LogUtil.w(LogUtil.BUGLE_TAG, "APN table query exception: " + e); |
| } catch (final SecurityException e) { |
| LogUtil.w(LogUtil.BUGLE_TAG, "Platform restricts APN table access: " + e); |
| throw e; |
| } |
| return null; |
| } |
| |
| /** |
| * Load matching APNs from local APN table. |
| * We try both using the APN name and not using the APN name. |
| * |
| * @param apnName the APN name |
| * @param apns the list of results to return |
| */ |
| private void loadFromLocalDatabase(final String apnName, final List<Apn> apns) { |
| LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from local APN table"); |
| final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase(); |
| final String mccMnc = PhoneUtils.getMccMncString(PhoneUtils.getDefault().getMccMnc()); |
| Cursor cursor = null; |
| cursor = queryLocalDatabase(database, mccMnc, apnName); |
| if (cursor == null) { |
| cursor = queryLocalDatabase(database, mccMnc, null/*apnName*/); |
| } |
| if (cursor == null) { |
| LogUtil.w(LogUtil.BUGLE_TAG, "Could not find any APN in local table"); |
| return; |
| } |
| try { |
| while (cursor.moveToNext()) { |
| final Apn apn = DatabaseApn.from(apns, |
| cursor.getString(COLUMN_TYPE), |
| cursor.getString(COLUMN_MMSC), |
| cursor.getString(COLUMN_MMSPROXY), |
| cursor.getString(COLUMN_MMSPORT), |
| cursor.getLong(COLUMN_ID), |
| cursor.getInt(COLUMN_CURRENT)); |
| if (apn != null) { |
| apns.add(apn); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * Make a query of local APN table based on MCC/MNC and APN name, sorted by CURRENT |
| * column in descending order |
| * |
| * @param db the local database |
| * @param numeric the MCC/MNC string |
| * @param apnName the optional APN name to match |
| * @return the cursor of the query, null if no result |
| */ |
| private static Cursor queryLocalDatabase(final SQLiteDatabase db, final String numeric, |
| final String apnName) { |
| final String selection; |
| final String[] selectionArgs; |
| if (TextUtils.isEmpty(apnName)) { |
| selection = SELECTION_NUMERIC; |
| selectionArgs = new String[] { numeric }; |
| } else { |
| selection = SELECTION_NUMERIC + " AND " + SELECTION_APN; |
| selectionArgs = new String[] { numeric, apnName }; |
| } |
| Cursor cursor = null; |
| try { |
| cursor = db.query(ApnDatabase.APN_TABLE, APN_PROJECTION_LOCAL, selection, selectionArgs, |
| null/*groupBy*/, null/*having*/, ORDER_BY, null/*limit*/); |
| } catch (final SQLiteException e) { |
| LogUtil.w(LogUtil.BUGLE_TAG, "Local APN table does not exist. Try rebuilding.", e); |
| ApnDatabase.forceBuildAndLoadApnTables(); |
| cursor = db.query(ApnDatabase.APN_TABLE, APN_PROJECTION_LOCAL, selection, selectionArgs, |
| null/*groupBy*/, null/*having*/, ORDER_BY, null/*limit*/); |
| } |
| if (cursor == null || cursor.getCount() < 1) { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| LogUtil.w(LogUtil.BUGLE_TAG, "Query local APNs with apn " + apnName |
| + " returned empty"); |
| return null; |
| } |
| return cursor; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Get the ID of first APN to try |
| */ |
| public static String getFirstTryApn(final SQLiteDatabase database, final String mccMnc) { |
| String key = null; |
| Cursor cursor = null; |
| try { |
| cursor = queryLocalDatabase(database, mccMnc, null/*apnName*/); |
| if (cursor.moveToFirst()) { |
| key = cursor.getString(ApnDatabase.COLUMN_ID); |
| } |
| } catch (final Exception e) { |
| // Nothing to do |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| return key; |
| } |
| } |