Home | History | Annotate | Download | only in mms
      1 /*
      2  * Copyright (C) 2015 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 android.support.v7.mms;
     18 
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.content.res.Resources;
     22 import android.content.res.XmlResourceParser;
     23 import android.database.Cursor;
     24 import android.database.sqlite.SQLiteException;
     25 import android.net.Uri;
     26 import android.provider.Telephony;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.util.SparseArray;
     30 
     31 import com.android.messaging.R;
     32 
     33 import java.net.URI;
     34 import java.net.URISyntaxException;
     35 import java.util.ArrayList;
     36 import java.util.List;
     37 
     38 /**
     39  * Default implementation of APN settings loader
     40  */
     41 class DefaultApnSettingsLoader implements ApnSettingsLoader {
     42     /**
     43      * The base implementation of an APN
     44      */
     45     private static class BaseApn implements Apn {
     46         /**
     47          * Create a base APN from parameters
     48          *
     49          * @param typesIn the APN type field
     50          * @param mmscIn the APN mmsc field
     51          * @param proxyIn the APN mmsproxy field
     52          * @param portIn the APN mmsport field
     53          * @return an instance of base APN, or null if any of the parameter is invalid
     54          */
     55         public static BaseApn from(final String typesIn, final String mmscIn, final String proxyIn,
     56                 final String portIn) {
     57             if (!isValidApnType(trimWithNullCheck(typesIn), APN_TYPE_MMS)) {
     58                 return null;
     59             }
     60             String mmsc = trimWithNullCheck(mmscIn);
     61             if (TextUtils.isEmpty(mmsc)) {
     62                 return null;
     63             }
     64             mmsc = trimV4AddrZeros(mmsc);
     65             try {
     66                 new URI(mmsc);
     67             } catch (final URISyntaxException e) {
     68                 return null;
     69             }
     70             String mmsProxy = trimWithNullCheck(proxyIn);
     71             int mmsProxyPort = 80;
     72             if (!TextUtils.isEmpty(mmsProxy)) {
     73                 mmsProxy = trimV4AddrZeros(mmsProxy);
     74                 final String portString = trimWithNullCheck(portIn);
     75                 if (portString != null) {
     76                     try {
     77                         mmsProxyPort = Integer.parseInt(portString);
     78                     } catch (final NumberFormatException e) {
     79                         // Ignore, just use 80 to try
     80                     }
     81                 }
     82             }
     83             return new BaseApn(mmsc, mmsProxy, mmsProxyPort);
     84         }
     85 
     86         private final String mMmsc;
     87         private final String mMmsProxy;
     88         private final int mMmsProxyPort;
     89 
     90         public BaseApn(final String mmsc, final String proxy, final int port) {
     91             mMmsc = mmsc;
     92             mMmsProxy = proxy;
     93             mMmsProxyPort = port;
     94         }
     95 
     96         @Override
     97         public String getMmsc() {
     98             return mMmsc;
     99         }
    100 
    101         @Override
    102         public String getMmsProxy() {
    103             return mMmsProxy;
    104         }
    105 
    106         @Override
    107         public int getMmsProxyPort() {
    108             return mMmsProxyPort;
    109         }
    110 
    111         @Override
    112         public void setSuccess() {
    113             // Do nothing
    114         }
    115 
    116         public boolean equals(final BaseApn other) {
    117             return TextUtils.equals(mMmsc, other.getMmsc()) &&
    118                     TextUtils.equals(mMmsProxy, other.getMmsProxy()) &&
    119                     mMmsProxyPort == other.getMmsProxyPort();
    120         }
    121     }
    122 
    123     /**
    124      * An in-memory implementation of an APN. These APNs are organized into an in-memory list.
    125      * The order of the list can be changed by the setSuccess method.
    126      */
    127     private static class MemoryApn implements Apn {
    128         /**
    129          * Create an in-memory APN loaded from resources
    130          *
    131          * @param apns the in-memory APN list
    132          * @param typesIn the APN type field
    133          * @param mmscIn the APN mmsc field
    134          * @param proxyIn the APN mmsproxy field
    135          * @param portIn the APN mmsport field
    136          * @return an in-memory APN instance, null if there is invalid parameter
    137          */
    138         public static MemoryApn from(final List<Apn> apns, final String typesIn,
    139                 final String mmscIn, final String proxyIn, final String portIn) {
    140             if (apns == null) {
    141                 return null;
    142             }
    143             final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn);
    144             if (base == null) {
    145                 return null;
    146             }
    147             for (final Apn apn : apns) {
    148                 if (apn instanceof MemoryApn && ((MemoryApn) apn).equals(base)) {
    149                     return null;
    150                 }
    151             }
    152             return new MemoryApn(apns, base);
    153         }
    154 
    155         private final List<Apn> mApns;
    156         private final BaseApn mBase;
    157 
    158         public MemoryApn(final List<Apn> apns, final BaseApn base) {
    159             mApns = apns;
    160             mBase = base;
    161         }
    162 
    163         @Override
    164         public String getMmsc() {
    165             return mBase.getMmsc();
    166         }
    167 
    168         @Override
    169         public String getMmsProxy() {
    170             return mBase.getMmsProxy();
    171         }
    172 
    173         @Override
    174         public int getMmsProxyPort() {
    175             return mBase.getMmsProxyPort();
    176         }
    177 
    178         @Override
    179         public void setSuccess() {
    180             // If this is being marked as a successful APN, move it to the top of the list so
    181             // next time it will be tried first
    182             boolean moved = false;
    183             synchronized (mApns) {
    184                 if (mApns.get(0) != this) {
    185                     mApns.remove(this);
    186                     mApns.add(0, this);
    187                     moved = true;
    188                 }
    189             }
    190             if (moved) {
    191                 Log.d(MmsService.TAG, "Set APN ["
    192                         + "MMSC=" + getMmsc() + ", "
    193                         + "PROXY=" + getMmsProxy() + ", "
    194                         + "PORT=" + getMmsProxyPort() + "] to be first");
    195             }
    196         }
    197 
    198         public boolean equals(final BaseApn other) {
    199             if (other == null) {
    200                 return false;
    201             }
    202             return mBase.equals(other);
    203         }
    204     }
    205 
    206     /**
    207      * APN_TYPE_ALL is a special type to indicate that this APN entry can
    208      * service all data connections.
    209      */
    210     public static final String APN_TYPE_ALL = "*";
    211     /** APN type for MMS traffic */
    212     public static final String APN_TYPE_MMS = "mms";
    213 
    214     private static final String[] APN_PROJECTION = {
    215             Telephony.Carriers.TYPE,
    216             Telephony.Carriers.MMSC,
    217             Telephony.Carriers.MMSPROXY,
    218             Telephony.Carriers.MMSPORT,
    219     };
    220     private static final int COLUMN_TYPE         = 0;
    221     private static final int COLUMN_MMSC         = 1;
    222     private static final int COLUMN_MMSPROXY     = 2;
    223     private static final int COLUMN_MMSPORT      = 3;
    224 
    225     private static final String APN_MCC = "mcc";
    226     private static final String APN_MNC = "mnc";
    227     private static final String APN_APN = "apn";
    228     private static final String APN_TYPE = "type";
    229     private static final String APN_MMSC = "mmsc";
    230     private static final String APN_MMSPROXY = "mmsproxy";
    231     private static final String APN_MMSPORT = "mmsport";
    232 
    233     private final Context mContext;
    234 
    235     // Cached APNs for subIds
    236     private final SparseArray<List<Apn>> mApnsCache;
    237 
    238     DefaultApnSettingsLoader(final Context context) {
    239         mContext = context;
    240         mApnsCache = new SparseArray<>();
    241     }
    242 
    243     @Override
    244     public List<Apn> get(final String apnName) {
    245         final int subId = Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID);
    246         List<Apn> apns;
    247         boolean didLoad = false;
    248         synchronized (this) {
    249             apns = mApnsCache.get(subId);
    250             if (apns == null) {
    251                 apns = new ArrayList<>();
    252                 mApnsCache.put(subId, apns);
    253                 loadLocked(subId, apnName, apns);
    254                 didLoad = true;
    255             }
    256         }
    257         if (didLoad) {
    258             Log.i(MmsService.TAG, "Loaded " + apns.size() + " APNs");
    259         }
    260         return apns;
    261     }
    262 
    263     private void loadLocked(final int subId, final String apnName, final List<Apn> apns) {
    264         // Try system APN table first
    265         loadFromSystem(subId, apnName, apns);
    266         if (apns.size() > 0) {
    267             return;
    268         }
    269         // Try loading from apns.xml in resources
    270         loadFromResources(subId, apnName, apns);
    271         if (apns.size() > 0) {
    272             return;
    273         }
    274         // Try resources but without APN name
    275         loadFromResources(subId, null/*apnName*/, apns);
    276     }
    277 
    278     /**
    279      * Load matching APNs from telephony provider.
    280      * We try different combinations of the query to work around some platform quirks.
    281      *
    282      * @param subId the SIM subId
    283      * @param apnName the APN name to match
    284      * @param apns the list used to return results
    285      */
    286     private void loadFromSystem(final int subId, final String apnName, final List<Apn> apns) {
    287         Uri uri;
    288         if (Utils.supportMSim() && subId != MmsManager.DEFAULT_SUB_ID) {
    289             uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "/subId/" + subId);
    290         } else {
    291             uri = Telephony.Carriers.CONTENT_URI;
    292         }
    293         Cursor cursor = null;
    294         try {
    295             for (; ; ) {
    296                 // Try different combinations of queries. Some would work on some platforms.
    297                 // So we query each combination until we find one returns non-empty result.
    298                 cursor = querySystem(uri, true/*checkCurrent*/, apnName);
    299                 if (cursor != null) {
    300                     break;
    301                 }
    302                 cursor = querySystem(uri, false/*checkCurrent*/, apnName);
    303                 if (cursor != null) {
    304                     break;
    305                 }
    306                 cursor = querySystem(uri, true/*checkCurrent*/, null/*apnName*/);
    307                 if (cursor != null) {
    308                     break;
    309                 }
    310                 cursor = querySystem(uri, false/*checkCurrent*/, null/*apnName*/);
    311                 break;
    312             }
    313         } catch (final SecurityException e) {
    314             // Can't access platform APN table, return directly
    315             return;
    316         }
    317         if (cursor == null) {
    318             return;
    319         }
    320         try {
    321             if (cursor.moveToFirst()) {
    322                 final Apn apn = BaseApn.from(
    323                         cursor.getString(COLUMN_TYPE),
    324                         cursor.getString(COLUMN_MMSC),
    325                         cursor.getString(COLUMN_MMSPROXY),
    326                         cursor.getString(COLUMN_MMSPORT));
    327                 if (apn != null) {
    328                     apns.add(apn);
    329                 }
    330             }
    331         } finally {
    332             cursor.close();
    333         }
    334     }
    335 
    336     /**
    337      * Query system APN table
    338      *
    339      * @param uri The APN query URL to use
    340      * @param checkCurrent If add "CURRENT IS NOT NULL" condition
    341      * @param apnName The optional APN name for query condition
    342      * @return A cursor of the query result. If a cursor is returned as not null, it is
    343      *         guaranteed to contain at least one row.
    344      */
    345     private Cursor querySystem(final Uri uri, final boolean checkCurrent, String apnName) {
    346         Log.i(MmsService.TAG, "Loading APNs from system, "
    347                 + "checkCurrent=" + checkCurrent + " apnName=" + apnName);
    348         final StringBuilder selectionBuilder = new StringBuilder();
    349         String[] selectionArgs = null;
    350         if (checkCurrent) {
    351             selectionBuilder.append(Telephony.Carriers.CURRENT).append(" IS NOT NULL");
    352         }
    353         apnName = trimWithNullCheck(apnName);
    354         if (!TextUtils.isEmpty(apnName)) {
    355             if (selectionBuilder.length() > 0) {
    356                 selectionBuilder.append(" AND ");
    357             }
    358             selectionBuilder.append(Telephony.Carriers.APN).append("=?");
    359             selectionArgs = new String[] { apnName };
    360         }
    361         try {
    362             final Cursor cursor = mContext.getContentResolver().query(
    363                     uri,
    364                     APN_PROJECTION,
    365                     selectionBuilder.toString(),
    366                     selectionArgs,
    367                     null/*sortOrder*/);
    368             if (cursor == null || cursor.getCount() < 1) {
    369                 if (cursor != null) {
    370                     cursor.close();
    371                 }
    372                 Log.w(MmsService.TAG, "Query " + uri + " with apn " + apnName + " and "
    373                         + (checkCurrent ? "checking CURRENT" : "not checking CURRENT")
    374                         + " returned empty");
    375                 return null;
    376             }
    377             return cursor;
    378         } catch (final SQLiteException e) {
    379             Log.w(MmsService.TAG, "APN table query exception: " + e);
    380         } catch (final SecurityException e) {
    381             Log.w(MmsService.TAG, "Platform restricts APN table access: " + e);
    382             throw e;
    383         }
    384         return null;
    385     }
    386 
    387     /**
    388      * Find matching APNs using builtin APN list resource
    389      *
    390      * @param subId the SIM subId
    391      * @param apnName the APN name to match
    392      * @param apns the list for returning results
    393      */
    394     private void loadFromResources(final int subId, final String apnName, final List<Apn> apns) {
    395         Log.i(MmsService.TAG, "Loading APNs from resources, apnName=" + apnName);
    396         final int[] mccMnc = Utils.getMccMnc(mContext, subId);
    397         if (mccMnc[0] == 0 && mccMnc[0] == 0) {
    398             Log.w(MmsService.TAG, "Can not get valid mcc/mnc from system");
    399             return;
    400         }
    401         // MCC/MNC is good, loading/querying APNs from XML
    402         XmlResourceParser xml = null;
    403         try {
    404             xml = mContext.getResources().getXml(R.xml.apns);
    405             new ApnsXmlParser(xml, new ApnsXmlParser.ApnProcessor() {
    406                 @Override
    407                 public void process(ContentValues apnValues) {
    408                     final String mcc = trimWithNullCheck(apnValues.getAsString(APN_MCC));
    409                     final String mnc = trimWithNullCheck(apnValues.getAsString(APN_MNC));
    410                     final String apn = trimWithNullCheck(apnValues.getAsString(APN_APN));
    411                     try {
    412                         if (mccMnc[0] == Integer.parseInt(mcc) &&
    413                                 mccMnc[1] == Integer.parseInt(mnc) &&
    414                                 (TextUtils.isEmpty(apnName) || apnName.equalsIgnoreCase(apn))) {
    415                             final String type = apnValues.getAsString(APN_TYPE);
    416                             final String mmsc = apnValues.getAsString(APN_MMSC);
    417                             final String mmsproxy = apnValues.getAsString(APN_MMSPROXY);
    418                             final String mmsport = apnValues.getAsString(APN_MMSPORT);
    419                             final Apn newApn = MemoryApn.from(apns, type, mmsc, mmsproxy, mmsport);
    420                             if (newApn != null) {
    421                                 apns.add(newApn);
    422                             }
    423                         }
    424                     } catch (final NumberFormatException e) {
    425                         // Ignore
    426                     }
    427                 }
    428             }).parse();
    429         } catch (final Resources.NotFoundException e) {
    430             Log.w(MmsService.TAG, "Can not get apns.xml " + e);
    431         } finally {
    432             if (xml != null) {
    433                 xml.close();
    434             }
    435         }
    436     }
    437 
    438     private static String trimWithNullCheck(final String value) {
    439         return value != null ? value.trim() : null;
    440     }
    441 
    442     /**
    443      * Trim leading zeros from IPv4 address strings
    444      * Our base libraries will interpret that as octel..
    445      * Must leave non v4 addresses and host names alone.
    446      * For example, 192.168.000.010 -> 192.168.0.10
    447      *
    448      * @param addr a string representing an ip addr
    449      * @return a string propertly trimmed
    450      */
    451     private static String trimV4AddrZeros(final String addr) {
    452         if (addr == null) {
    453             return null;
    454         }
    455         final String[] octets = addr.split("\\.");
    456         if (octets.length != 4) {
    457             return addr;
    458         }
    459         final StringBuilder builder = new StringBuilder(16);
    460         String result = null;
    461         for (int i = 0; i < 4; i++) {
    462             try {
    463                 if (octets[i].length() > 3) {
    464                     return addr;
    465                 }
    466                 builder.append(Integer.parseInt(octets[i]));
    467             } catch (final NumberFormatException e) {
    468                 return addr;
    469             }
    470             if (i < 3) {
    471                 builder.append('.');
    472             }
    473         }
    474         result = builder.toString();
    475         return result;
    476     }
    477 
    478     /**
    479      * Check if the APN contains the APN type we want
    480      *
    481      * @param types The string encodes a list of supported types
    482      * @param requestType The type we want
    483      * @return true if the input types string contains the requestType
    484      */
    485     public static boolean isValidApnType(final String types, final String requestType) {
    486         // If APN type is unspecified, assume APN_TYPE_ALL.
    487         if (TextUtils.isEmpty(types)) {
    488             return true;
    489         }
    490         for (final String t : types.split(",")) {
    491             if (t.equals(requestType) || t.equals(APN_TYPE_ALL)) {
    492                 return true;
    493             }
    494         }
    495         return false;
    496     }
    497 }
    498