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.app.Activity;
     20 import android.app.PendingIntent;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.net.ConnectivityManager;
     24 import android.net.Uri;
     25 import android.os.Bundle;
     26 import android.os.Parcel;
     27 import android.os.Parcelable;
     28 import android.support.v7.mms.pdu.GenericPdu;
     29 import android.support.v7.mms.pdu.PduHeaders;
     30 import android.support.v7.mms.pdu.PduParser;
     31 import android.support.v7.mms.pdu.SendConf;
     32 import android.telephony.SmsManager;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 
     36 import java.lang.reflect.Method;
     37 import java.net.Inet4Address;
     38 import java.net.InetAddress;
     39 import java.net.UnknownHostException;
     40 import java.util.List;
     41 import java.util.concurrent.ExecutorService;
     42 import java.util.concurrent.Executors;
     43 
     44 /**
     45  * MMS request base class. This handles the execution of any MMS request.
     46  */
     47 abstract class MmsRequest implements Parcelable {
     48     /**
     49      * Prepare to make the HTTP request - will download message for sending
     50      *
     51      * @param context the Context
     52      * @param mmsConfig carrier config values to use
     53      * @return true if loading request PDU from calling app succeeds, false otherwise
     54      */
     55     protected abstract boolean loadRequest(Context context, Bundle mmsConfig);
     56 
     57     /**
     58      * Transfer the received response to the caller
     59      *
     60      * @param context the Context
     61      * @param fillIn the content of pending intent to be returned
     62      * @param response the pdu to transfer
     63      * @return true if transferring response PDU to calling app succeeds, false otherwise
     64      */
     65     protected abstract boolean transferResponse(Context context, Intent fillIn, byte[] response);
     66 
     67     /**
     68      * Making the HTTP request to MMSC
     69      *
     70      * @param context The context
     71      * @param netMgr The current {@link MmsNetworkManager}
     72      * @param apn The APN
     73      * @param mmsConfig The carrier configuration values to use
     74      * @param userAgent The User-Agent header value
     75      * @param uaProfUrl The UA Prof URL header value
     76      * @return The HTTP response data
     77      * @throws MmsHttpException If any network error happens
     78      */
     79     protected abstract byte[] doHttp(Context context, MmsNetworkManager netMgr,
     80             ApnSettingsLoader.Apn apn, Bundle mmsConfig, String userAgent, String uaProfUrl)
     81             throws MmsHttpException;
     82 
     83     /**
     84      * Get the HTTP request URL for this MMS request
     85      *
     86      * @param apn The APN to use
     87      * @return The HTTP request URL in text
     88      */
     89     protected abstract String getHttpRequestUrl(ApnSettingsLoader.Apn apn);
     90 
     91     // Maximum time to spend waiting to read data from a content provider before failing with error.
     92     protected static final int TASK_TIMEOUT_MS = 30 * 1000;
     93 
     94     protected final String mLocationUrl;
     95     protected final Uri mPduUri;
     96     protected final PendingIntent mPendingIntent;
     97     // Thread pool for transferring PDU with MMS apps
     98     protected final ExecutorService mPduTransferExecutor = Executors.newCachedThreadPool();
     99 
    100     // Whether this request should acquire wake lock
    101     private boolean mUseWakeLock;
    102 
    103     protected MmsRequest(final String locationUrl, final Uri pduUri,
    104             final PendingIntent pendingIntent) {
    105         mLocationUrl = locationUrl;
    106         mPduUri = pduUri;
    107         mPendingIntent = pendingIntent;
    108         mUseWakeLock = true;
    109     }
    110 
    111     void setUseWakeLock(final boolean useWakeLock) {
    112         mUseWakeLock = useWakeLock;
    113     }
    114 
    115     boolean getUseWakeLock() {
    116         return mUseWakeLock;
    117     }
    118 
    119     /**
    120      * Run the MMS request.
    121      *
    122      * @param context the context to use
    123      * @param networkManager the MmsNetworkManager to use to setup MMS network
    124      * @param apnSettingsLoader the APN loader
    125      * @param carrierConfigValuesLoader the carrier config loader
    126      * @param userAgentInfoLoader the user agent info loader
    127      */
    128     void execute(final Context context, final MmsNetworkManager networkManager,
    129             final ApnSettingsLoader apnSettingsLoader,
    130             final CarrierConfigValuesLoader carrierConfigValuesLoader,
    131             final UserAgentInfoLoader userAgentInfoLoader) {
    132         Log.i(MmsService.TAG, "Execute " + this.getClass().getSimpleName());
    133         int result = SmsManager.MMS_ERROR_UNSPECIFIED;
    134         int httpStatusCode = 0;
    135         byte[] response = null;
    136         final Bundle mmsConfig = carrierConfigValuesLoader.get(MmsManager.DEFAULT_SUB_ID);
    137         if (mmsConfig == null) {
    138             Log.e(MmsService.TAG, "Failed to load carrier configuration values");
    139             result = SmsManager.MMS_ERROR_CONFIGURATION_ERROR;
    140         } else if (!loadRequest(context, mmsConfig)) {
    141             Log.e(MmsService.TAG, "Failed to load PDU");
    142             result = SmsManager.MMS_ERROR_IO_ERROR;
    143         } else {
    144             // Everything's OK. Now execute the request.
    145             try {
    146                 // Acquire the MMS network
    147                 networkManager.acquireNetwork();
    148                 // Load the potential APNs. In most cases there should be only one APN available.
    149                 // On some devices on which we can't obtain APN from system, we look up our own
    150                 // APN list. Since we don't have exact information, we may get a list of potential
    151                 // APNs to try. Whenever we found a successful APN, we signal it and return.
    152                 final String apnName = networkManager.getApnName();
    153                 final List<ApnSettingsLoader.Apn> apns = apnSettingsLoader.get(apnName);
    154                 if (apns.size() < 1) {
    155                     throw new ApnException("No valid APN");
    156                 } else {
    157                     Log.d(MmsService.TAG, "Trying " + apns.size() + " APNs");
    158                 }
    159                 final String userAgent = userAgentInfoLoader.getUserAgent();
    160                 final String uaProfUrl = userAgentInfoLoader.getUAProfUrl();
    161                 MmsHttpException lastException = null;
    162                 for (ApnSettingsLoader.Apn apn : apns) {
    163                     Log.i(MmsService.TAG, "Using APN ["
    164                             + "MMSC=" + apn.getMmsc() + ", "
    165                             + "PROXY=" + apn.getMmsProxy() + ", "
    166                             + "PORT=" + apn.getMmsProxyPort() + "]");
    167                     try {
    168                         final String url = getHttpRequestUrl(apn);
    169                         // Request a global route for the host to connect
    170                         requestRoute(networkManager.getConnectivityManager(), apn, url);
    171                         // Perform the HTTP request
    172                         response = doHttp(
    173                                 context, networkManager, apn, mmsConfig, userAgent, uaProfUrl);
    174                         // Additional check of whether this is a success
    175                         if (isWrongApnResponse(response, mmsConfig)) {
    176                             throw new MmsHttpException(0/*statusCode*/, "Invalid sending address");
    177                         }
    178                         // Notify APN loader this is a valid APN
    179                         apn.setSuccess();
    180                         result = Activity.RESULT_OK;
    181                         break;
    182                     } catch (MmsHttpException e) {
    183                         Log.w(MmsService.TAG, "HTTP or network failure", e);
    184                         lastException = e;
    185                     }
    186                 }
    187                 if (lastException != null) {
    188                     throw lastException;
    189                 }
    190             } catch (ApnException e) {
    191                 Log.e(MmsService.TAG, "MmsRequest: APN failure", e);
    192                 result = SmsManager.MMS_ERROR_INVALID_APN;
    193             } catch (MmsNetworkException e) {
    194                 Log.e(MmsService.TAG, "MmsRequest: MMS network acquiring failure", e);
    195                 result = SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS;
    196             } catch (MmsHttpException e) {
    197                 Log.e(MmsService.TAG, "MmsRequest: HTTP or network I/O failure", e);
    198                 result = SmsManager.MMS_ERROR_HTTP_FAILURE;
    199                 httpStatusCode = e.getStatusCode();
    200             } catch (Exception e) {
    201                 Log.e(MmsService.TAG, "MmsRequest: unexpected failure", e);
    202                 result = SmsManager.MMS_ERROR_UNSPECIFIED;
    203             } finally {
    204                 // Release MMS network
    205                 networkManager.releaseNetwork();
    206             }
    207         }
    208         // Process result and send back via PendingIntent
    209         returnResult(context, result, response, httpStatusCode);
    210     }
    211 
    212     /**
    213      * Check if the response indicates a failure when we send to wrong APN.
    214      * Sometimes even if you send to the wrong APN, a response in valid PDU format can still
    215      * be sent back but with an error status. Check one specific case here.
    216      *
    217      * TODO: maybe there are other possibilities.
    218      *
    219      * @param response the response data
    220      * @param mmsConfig the carrier configuration values to use
    221      * @return false if we find an invalid response case, otherwise true
    222      */
    223     static boolean isWrongApnResponse(final byte[] response, final Bundle mmsConfig) {
    224         if (response != null && response.length > 0) {
    225             try {
    226                 final GenericPdu pdu = new PduParser(
    227                         response,
    228                         mmsConfig.getBoolean(
    229                                 CarrierConfigValuesLoader
    230                                         .CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION,
    231                                 CarrierConfigValuesLoader
    232                                         .CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION_DEFAULT))
    233                         .parse();
    234                 if (pdu != null && pdu instanceof SendConf) {
    235                     final SendConf sendConf = (SendConf) pdu;
    236                     final int responseStatus = sendConf.getResponseStatus();
    237                     return responseStatus ==
    238                             PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED ||
    239                             responseStatus ==
    240                                     PduHeaders.RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED;
    241                 }
    242             } catch (RuntimeException e) {
    243                 Log.w(MmsService.TAG, "Parsing response failed", e);
    244             }
    245         }
    246         return false;
    247     }
    248 
    249     /**
    250      * Return the result back via pending intent
    251      *
    252      * @param context The context
    253      * @param result The result code of execution
    254      * @param response The response body
    255      * @param httpStatusCode The optional http status code in case of http failure
    256      */
    257     void returnResult(final Context context, int result, final byte[] response,
    258             final int httpStatusCode) {
    259         if (mPendingIntent == null) {
    260             // Result not needed
    261             return;
    262         }
    263         // Extra information to send back with the pending intent
    264         final Intent fillIn = new Intent();
    265         if (response != null) {
    266             if (!transferResponse(context, fillIn, response)) {
    267                 // Failed to send PDU data back to caller
    268                 result = SmsManager.MMS_ERROR_IO_ERROR;
    269             }
    270         }
    271         if (result == SmsManager.MMS_ERROR_HTTP_FAILURE && httpStatusCode != 0) {
    272             // For HTTP failure, fill in the status code for more information
    273             fillIn.putExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, httpStatusCode);
    274         }
    275         try {
    276             mPendingIntent.send(context, result, fillIn);
    277         } catch (PendingIntent.CanceledException e) {
    278             Log.e(MmsService.TAG, "Sending pending intent canceled", e);
    279         }
    280     }
    281 
    282     /**
    283      * Request the route to the APN (either proxy host or the MMSC host)
    284      *
    285      * @param connectivityManager the ConnectivityManager to use
    286      * @param apn the current APN
    287      * @param url the URL to connect to
    288      * @throws MmsHttpException for unknown host or route failure
    289      */
    290     private static void requestRoute(final ConnectivityManager connectivityManager,
    291             final ApnSettingsLoader.Apn apn, final String url) throws MmsHttpException {
    292         String host = apn.getMmsProxy();
    293         if (TextUtils.isEmpty(host)) {
    294             final Uri uri = Uri.parse(url);
    295             host = uri.getHost();
    296         }
    297         boolean success = false;
    298         // Request route to all resolved host addresses
    299         try {
    300             for (final InetAddress addr : InetAddress.getAllByName(host)) {
    301                 final boolean requested = requestRouteToHostAddress(connectivityManager, addr);
    302                 if (requested) {
    303                     success = true;
    304                     Log.i(MmsService.TAG, "Requested route to " + addr);
    305                 } else {
    306                     Log.i(MmsService.TAG, "Could not requested route to " + addr);
    307                 }
    308             }
    309             if (!success) {
    310                 throw new MmsHttpException(0/*statusCode*/, "No route requested");
    311             }
    312         } catch (UnknownHostException e) {
    313             Log.w(MmsService.TAG, "Unknown host " + host);
    314             throw new MmsHttpException(0/*statusCode*/, "Unknown host");
    315         }
    316     }
    317 
    318     private static final Integer TYPE_MOBILE_MMS =
    319             Integer.valueOf(ConnectivityManager.TYPE_MOBILE_MMS);
    320     /**
    321      * Wrapper for platform API requestRouteToHostAddress
    322      *
    323      * We first try the hidden but correct method on ConnectivityManager. If we can't, use
    324      * the old but buggy one
    325      *
    326      * @param connMgr the ConnectivityManager instance
    327      * @param inetAddr the InetAddress to request
    328      * @return true if route is successfully setup, false otherwise
    329      */
    330     private static boolean requestRouteToHostAddress(final ConnectivityManager connMgr,
    331             final InetAddress inetAddr) {
    332         // First try the good method using reflection
    333         try {
    334             final Method method = connMgr.getClass().getMethod("requestRouteToHostAddress",
    335                     Integer.TYPE, InetAddress.class);
    336             if (method != null) {
    337                 return (Boolean) method.invoke(connMgr, TYPE_MOBILE_MMS, inetAddr);
    338             }
    339         } catch (Exception e) {
    340             Log.w(MmsService.TAG, "ConnectivityManager.requestRouteToHostAddress failed " + e);
    341         }
    342         // If we fail, try the old but buggy one
    343         if (inetAddr instanceof Inet4Address) {
    344             try {
    345                 final Method method = connMgr.getClass().getMethod("requestRouteToHost",
    346                         Integer.TYPE, Integer.TYPE);
    347                 if (method != null) {
    348                     return (Boolean) method.invoke(connMgr, TYPE_MOBILE_MMS,
    349                         inetAddressToInt(inetAddr));
    350                 }
    351             } catch (Exception e) {
    352                 Log.w(MmsService.TAG, "ConnectivityManager.requestRouteToHost failed " + e);
    353             }
    354         }
    355         return false;
    356     }
    357 
    358     /**
    359      * Convert a IPv4 address from an InetAddress to an integer
    360      *
    361      * @param inetAddr is an InetAddress corresponding to the IPv4 address
    362      * @return the IP address as an integer in network byte order
    363      */
    364     private static int inetAddressToInt(final InetAddress inetAddr)
    365             throws IllegalArgumentException {
    366         final byte [] addr = inetAddr.getAddress();
    367         return ((addr[3] & 0xff) << 24) | ((addr[2] & 0xff) << 16) |
    368                 ((addr[1] & 0xff) << 8) | (addr[0] & 0xff);
    369     }
    370 
    371     @Override
    372     public int describeContents() {
    373         return 0;
    374     }
    375 
    376     @Override
    377     public void writeToParcel(Parcel parcel, int flags) {
    378         parcel.writeByte((byte) (mUseWakeLock ? 1 : 0));
    379         parcel.writeString(mLocationUrl);
    380         parcel.writeParcelable(mPduUri, 0);
    381         parcel.writeParcelable(mPendingIntent, 0);
    382     }
    383 
    384     protected MmsRequest(final Parcel in) {
    385         final ClassLoader classLoader = MmsRequest.class.getClassLoader();
    386         mUseWakeLock = in.readByte() != 0;
    387         mLocationUrl = in.readString();
    388         mPduUri = in.readParcelable(classLoader);
    389         mPendingIntent = in.readParcelable(classLoader);
    390     }
    391 }
    392