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