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