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      */
loadRequest(Context context, Bundle mmsConfig)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      */
transferResponse(Context context, Intent fillIn, byte[] response)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      */
doHttp(Context context, MmsNetworkManager netMgr, ApnSettingsLoader.Apn apn, Bundle mmsConfig, String userAgent, String uaProfUrl)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      */
getHttpRequestUrl(ApnSettingsLoader.Apn apn)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 
MmsRequest(final String locationUrl, final Uri pduUri, final PendingIntent pendingIntent)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 
setUseWakeLock(final boolean useWakeLock)111     void setUseWakeLock(final boolean useWakeLock) {
112         mUseWakeLock = useWakeLock;
113     }
114 
getUseWakeLock()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      */
execute(final Context context, final MmsNetworkManager networkManager, final ApnSettingsLoader apnSettingsLoader, final CarrierConfigValuesLoader carrierConfigValuesLoader, final UserAgentInfoLoader userAgentInfoLoader)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      */
isWrongApnResponse(final byte[] response, final Bundle mmsConfig)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      */
returnResult(final Context context, int result, final byte[] response, final int httpStatusCode)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      */
requestRoute(final ConnectivityManager connectivityManager, final ApnSettingsLoader.Apn apn, final String url)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      */
requestRouteToHostAddress(final ConnectivityManager connMgr, final InetAddress inetAddr)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      */
inetAddressToInt(final InetAddress inetAddr)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
describeContents()372     public int describeContents() {
373         return 0;
374     }
375 
376     @Override
writeToParcel(Parcel parcel, int flags)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 
MmsRequest(final Parcel in)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