| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.support.v7.mms; |
| |
| import android.app.Activity; |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.net.ConnectivityManager; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.support.v7.mms.pdu.GenericPdu; |
| import android.support.v7.mms.pdu.PduHeaders; |
| import android.support.v7.mms.pdu.PduParser; |
| import android.support.v7.mms.pdu.SendConf; |
| import android.telephony.SmsManager; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import java.lang.reflect.Method; |
| import java.net.Inet4Address; |
| import java.net.InetAddress; |
| import java.net.UnknownHostException; |
| import java.util.List; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| |
| /** |
| * MMS request base class. This handles the execution of any MMS request. |
| */ |
| abstract class MmsRequest implements Parcelable { |
| /** |
| * Prepare to make the HTTP request - will download message for sending |
| * |
| * @param context the Context |
| * @param mmsConfig carrier config values to use |
| * @return true if loading request PDU from calling app succeeds, false otherwise |
| */ |
| protected abstract boolean loadRequest(Context context, Bundle mmsConfig); |
| |
| /** |
| * Transfer the received response to the caller |
| * |
| * @param context the Context |
| * @param fillIn the content of pending intent to be returned |
| * @param response the pdu to transfer |
| * @return true if transferring response PDU to calling app succeeds, false otherwise |
| */ |
| protected abstract boolean transferResponse(Context context, Intent fillIn, byte[] response); |
| |
| /** |
| * Making the HTTP request to MMSC |
| * |
| * @param context The context |
| * @param netMgr The current {@link MmsNetworkManager} |
| * @param apn The APN |
| * @param mmsConfig The carrier configuration values to use |
| * @param userAgent The User-Agent header value |
| * @param uaProfUrl The UA Prof URL header value |
| * @return The HTTP response data |
| * @throws MmsHttpException If any network error happens |
| */ |
| protected abstract byte[] doHttp(Context context, MmsNetworkManager netMgr, |
| ApnSettingsLoader.Apn apn, Bundle mmsConfig, String userAgent, String uaProfUrl) |
| throws MmsHttpException; |
| |
| /** |
| * Get the HTTP request URL for this MMS request |
| * |
| * @param apn The APN to use |
| * @return The HTTP request URL in text |
| */ |
| protected abstract String getHttpRequestUrl(ApnSettingsLoader.Apn apn); |
| |
| // Maximum time to spend waiting to read data from a content provider before failing with error. |
| protected static final int TASK_TIMEOUT_MS = 30 * 1000; |
| |
| protected final String mLocationUrl; |
| protected final Uri mPduUri; |
| protected final PendingIntent mPendingIntent; |
| // Thread pool for transferring PDU with MMS apps |
| protected final ExecutorService mPduTransferExecutor = Executors.newCachedThreadPool(); |
| |
| // Whether this request should acquire wake lock |
| private boolean mUseWakeLock; |
| |
| protected MmsRequest(final String locationUrl, final Uri pduUri, |
| final PendingIntent pendingIntent) { |
| mLocationUrl = locationUrl; |
| mPduUri = pduUri; |
| mPendingIntent = pendingIntent; |
| mUseWakeLock = true; |
| } |
| |
| void setUseWakeLock(final boolean useWakeLock) { |
| mUseWakeLock = useWakeLock; |
| } |
| |
| boolean getUseWakeLock() { |
| return mUseWakeLock; |
| } |
| |
| /** |
| * Run the MMS request. |
| * |
| * @param context the context to use |
| * @param networkManager the MmsNetworkManager to use to setup MMS network |
| * @param apnSettingsLoader the APN loader |
| * @param carrierConfigValuesLoader the carrier config loader |
| * @param userAgentInfoLoader the user agent info loader |
| */ |
| void execute(final Context context, final MmsNetworkManager networkManager, |
| final ApnSettingsLoader apnSettingsLoader, |
| final CarrierConfigValuesLoader carrierConfigValuesLoader, |
| final UserAgentInfoLoader userAgentInfoLoader) { |
| Log.i(MmsService.TAG, "Execute " + this.getClass().getSimpleName()); |
| int result = SmsManager.MMS_ERROR_UNSPECIFIED; |
| int httpStatusCode = 0; |
| byte[] response = null; |
| final Bundle mmsConfig = carrierConfigValuesLoader.get(MmsManager.DEFAULT_SUB_ID); |
| if (mmsConfig == null) { |
| Log.e(MmsService.TAG, "Failed to load carrier configuration values"); |
| result = SmsManager.MMS_ERROR_CONFIGURATION_ERROR; |
| } else if (!loadRequest(context, mmsConfig)) { |
| Log.e(MmsService.TAG, "Failed to load PDU"); |
| result = SmsManager.MMS_ERROR_IO_ERROR; |
| } else { |
| // Everything's OK. Now execute the request. |
| try { |
| // Acquire the MMS network |
| networkManager.acquireNetwork(); |
| // Load the potential APNs. In most cases there should be only one APN available. |
| // On some devices on which we can't obtain APN from system, we look up our own |
| // APN list. Since we don't have exact information, we may get a list of potential |
| // APNs to try. Whenever we found a successful APN, we signal it and return. |
| final String apnName = networkManager.getApnName(); |
| final List<ApnSettingsLoader.Apn> apns = apnSettingsLoader.get(apnName); |
| if (apns.size() < 1) { |
| throw new ApnException("No valid APN"); |
| } else { |
| Log.d(MmsService.TAG, "Trying " + apns.size() + " APNs"); |
| } |
| final String userAgent = userAgentInfoLoader.getUserAgent(); |
| final String uaProfUrl = userAgentInfoLoader.getUAProfUrl(); |
| MmsHttpException lastException = null; |
| for (ApnSettingsLoader.Apn apn : apns) { |
| Log.i(MmsService.TAG, "Using APN [" |
| + "MMSC=" + apn.getMmsc() + ", " |
| + "PROXY=" + apn.getMmsProxy() + ", " |
| + "PORT=" + apn.getMmsProxyPort() + "]"); |
| try { |
| final String url = getHttpRequestUrl(apn); |
| // Request a global route for the host to connect |
| requestRoute(networkManager.getConnectivityManager(), apn, url); |
| // Perform the HTTP request |
| response = doHttp( |
| context, networkManager, apn, mmsConfig, userAgent, uaProfUrl); |
| // Additional check of whether this is a success |
| if (isWrongApnResponse(response, mmsConfig)) { |
| throw new MmsHttpException(0/*statusCode*/, "Invalid sending address"); |
| } |
| // Notify APN loader this is a valid APN |
| apn.setSuccess(); |
| result = Activity.RESULT_OK; |
| break; |
| } catch (MmsHttpException e) { |
| Log.w(MmsService.TAG, "HTTP or network failure", e); |
| lastException = e; |
| } |
| } |
| if (lastException != null) { |
| throw lastException; |
| } |
| } catch (ApnException e) { |
| Log.e(MmsService.TAG, "MmsRequest: APN failure", e); |
| result = SmsManager.MMS_ERROR_INVALID_APN; |
| } catch (MmsNetworkException e) { |
| Log.e(MmsService.TAG, "MmsRequest: MMS network acquiring failure", e); |
| result = SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS; |
| } catch (MmsHttpException e) { |
| Log.e(MmsService.TAG, "MmsRequest: HTTP or network I/O failure", e); |
| result = SmsManager.MMS_ERROR_HTTP_FAILURE; |
| httpStatusCode = e.getStatusCode(); |
| } catch (Exception e) { |
| Log.e(MmsService.TAG, "MmsRequest: unexpected failure", e); |
| result = SmsManager.MMS_ERROR_UNSPECIFIED; |
| } finally { |
| // Release MMS network |
| networkManager.releaseNetwork(); |
| } |
| } |
| // Process result and send back via PendingIntent |
| returnResult(context, result, response, httpStatusCode); |
| } |
| |
| /** |
| * Check if the response indicates a failure when we send to wrong APN. |
| * Sometimes even if you send to the wrong APN, a response in valid PDU format can still |
| * be sent back but with an error status. Check one specific case here. |
| * |
| * TODO: maybe there are other possibilities. |
| * |
| * @param response the response data |
| * @param mmsConfig the carrier configuration values to use |
| * @return false if we find an invalid response case, otherwise true |
| */ |
| static boolean isWrongApnResponse(final byte[] response, final Bundle mmsConfig) { |
| if (response != null && response.length > 0) { |
| try { |
| final GenericPdu pdu = new PduParser( |
| response, |
| mmsConfig.getBoolean( |
| CarrierConfigValuesLoader |
| .CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION, |
| CarrierConfigValuesLoader |
| .CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION_DEFAULT)) |
| .parse(); |
| if (pdu != null && pdu instanceof SendConf) { |
| final SendConf sendConf = (SendConf) pdu; |
| final int responseStatus = sendConf.getResponseStatus(); |
| return responseStatus == |
| PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED || |
| responseStatus == |
| PduHeaders.RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED; |
| } |
| } catch (RuntimeException e) { |
| Log.w(MmsService.TAG, "Parsing response failed", e); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return the result back via pending intent |
| * |
| * @param context The context |
| * @param result The result code of execution |
| * @param response The response body |
| * @param httpStatusCode The optional http status code in case of http failure |
| */ |
| void returnResult(final Context context, int result, final byte[] response, |
| final int httpStatusCode) { |
| if (mPendingIntent == null) { |
| // Result not needed |
| return; |
| } |
| // Extra information to send back with the pending intent |
| final Intent fillIn = new Intent(); |
| if (response != null) { |
| if (!transferResponse(context, fillIn, response)) { |
| // Failed to send PDU data back to caller |
| result = SmsManager.MMS_ERROR_IO_ERROR; |
| } |
| } |
| if (result == SmsManager.MMS_ERROR_HTTP_FAILURE && httpStatusCode != 0) { |
| // For HTTP failure, fill in the status code for more information |
| fillIn.putExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, httpStatusCode); |
| } |
| try { |
| mPendingIntent.send(context, result, fillIn); |
| } catch (PendingIntent.CanceledException e) { |
| Log.e(MmsService.TAG, "Sending pending intent canceled", e); |
| } |
| } |
| |
| /** |
| * Request the route to the APN (either proxy host or the MMSC host) |
| * |
| * @param connectivityManager the ConnectivityManager to use |
| * @param apn the current APN |
| * @param url the URL to connect to |
| * @throws MmsHttpException for unknown host or route failure |
| */ |
| private static void requestRoute(final ConnectivityManager connectivityManager, |
| final ApnSettingsLoader.Apn apn, final String url) throws MmsHttpException { |
| String host = apn.getMmsProxy(); |
| if (TextUtils.isEmpty(host)) { |
| final Uri uri = Uri.parse(url); |
| host = uri.getHost(); |
| } |
| boolean success = false; |
| // Request route to all resolved host addresses |
| try { |
| for (final InetAddress addr : InetAddress.getAllByName(host)) { |
| final boolean requested = requestRouteToHostAddress(connectivityManager, addr); |
| if (requested) { |
| success = true; |
| Log.i(MmsService.TAG, "Requested route to " + addr); |
| } else { |
| Log.i(MmsService.TAG, "Could not requested route to " + addr); |
| } |
| } |
| if (!success) { |
| throw new MmsHttpException(0/*statusCode*/, "No route requested"); |
| } |
| } catch (UnknownHostException e) { |
| Log.w(MmsService.TAG, "Unknown host " + host); |
| throw new MmsHttpException(0/*statusCode*/, "Unknown host"); |
| } |
| } |
| |
| private static final Integer TYPE_MOBILE_MMS = |
| Integer.valueOf(ConnectivityManager.TYPE_MOBILE_MMS); |
| /** |
| * Wrapper for platform API requestRouteToHostAddress |
| * |
| * We first try the hidden but correct method on ConnectivityManager. If we can't, use |
| * the old but buggy one |
| * |
| * @param connMgr the ConnectivityManager instance |
| * @param inetAddr the InetAddress to request |
| * @return true if route is successfully setup, false otherwise |
| */ |
| private static boolean requestRouteToHostAddress(final ConnectivityManager connMgr, |
| final InetAddress inetAddr) { |
| // First try the good method using reflection |
| try { |
| final Method method = connMgr.getClass().getMethod("requestRouteToHostAddress", |
| Integer.TYPE, InetAddress.class); |
| if (method != null) { |
| return (Boolean) method.invoke(connMgr, TYPE_MOBILE_MMS, inetAddr); |
| } |
| } catch (Exception e) { |
| Log.w(MmsService.TAG, "ConnectivityManager.requestRouteToHostAddress failed " + e); |
| } |
| // If we fail, try the old but buggy one |
| if (inetAddr instanceof Inet4Address) { |
| try { |
| final Method method = connMgr.getClass().getMethod("requestRouteToHost", |
| Integer.TYPE, Integer.TYPE); |
| if (method != null) { |
| return (Boolean) method.invoke(connMgr, TYPE_MOBILE_MMS, |
| inetAddressToInt(inetAddr)); |
| } |
| } catch (Exception e) { |
| Log.w(MmsService.TAG, "ConnectivityManager.requestRouteToHost failed " + e); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Convert a IPv4 address from an InetAddress to an integer |
| * |
| * @param inetAddr is an InetAddress corresponding to the IPv4 address |
| * @return the IP address as an integer in network byte order |
| */ |
| private static int inetAddressToInt(final InetAddress inetAddr) |
| throws IllegalArgumentException { |
| final byte [] addr = inetAddr.getAddress(); |
| return ((addr[3] & 0xff) << 24) | ((addr[2] & 0xff) << 16) | |
| ((addr[1] & 0xff) << 8) | (addr[0] & 0xff); |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel parcel, int flags) { |
| parcel.writeByte((byte) (mUseWakeLock ? 1 : 0)); |
| parcel.writeString(mLocationUrl); |
| parcel.writeParcelable(mPduUri, 0); |
| parcel.writeParcelable(mPendingIntent, 0); |
| } |
| |
| protected MmsRequest(final Parcel in) { |
| final ClassLoader classLoader = MmsRequest.class.getClassLoader(); |
| mUseWakeLock = in.readByte() != 0; |
| mLocationUrl = in.readString(); |
| mPduUri = in.readParcelable(classLoader); |
| mPendingIntent = in.readParcelable(classLoader); |
| } |
| } |