diff options
-rw-r--r-- | Android.mk | 2 | ||||
-rw-r--r-- | src/com/android/mms/service/DownloadRequest.java | 36 | ||||
-rw-r--r-- | src/com/android/mms/service/HttpUtils.java | 418 | ||||
-rw-r--r-- | src/com/android/mms/service/MmsHttpClient.java | 377 | ||||
-rw-r--r-- | src/com/android/mms/service/MmsNetworkManager.java | 67 | ||||
-rw-r--r-- | src/com/android/mms/service/MmsRequest.java | 172 | ||||
-rw-r--r-- | src/com/android/mms/service/SendRequest.java | 34 | ||||
-rw-r--r-- | src/com/android/mms/service/http/NameResolver.java | 27 | ||||
-rw-r--r-- | src/com/android/mms/service/http/NetworkAwareClientConnectionOperator.java | 185 | ||||
-rw-r--r-- | src/com/android/mms/service/http/NetworkAwareHttpClient.java | 504 | ||||
-rw-r--r-- | src/com/android/mms/service/http/NetworkAwareThreadSafeClientConnManager.java | 40 |
11 files changed, 478 insertions, 1384 deletions
@@ -22,7 +22,7 @@ include $(CLEAR_VARS) LOCAL_PACKAGE_NAME := MmsService LOCAL_PRIVILEGED_MODULE := true -LOCAL_JAVA_LIBRARIES := telephony-common +LOCAL_JAVA_LIBRARIES := telephony-common okhttp LOCAL_SRC_FILES := $(call all-java-files-under, src) diff --git a/src/com/android/mms/service/DownloadRequest.java b/src/com/android/mms/service/DownloadRequest.java index 9c5ac14..3805d1c 100644 --- a/src/com/android/mms/service/DownloadRequest.java +++ b/src/com/android/mms/service/DownloadRequest.java @@ -16,16 +16,6 @@ package com.android.mms.service; -import com.google.android.mms.MmsException; -import com.google.android.mms.pdu.GenericPdu; -import com.google.android.mms.pdu.PduHeaders; -import com.google.android.mms.pdu.PduParser; -import com.google.android.mms.pdu.PduPersister; -import com.google.android.mms.pdu.RetrieveConf; -import com.google.android.mms.util.SqliteWrapper; - -import com.android.mms.service.exception.MmsHttpException; - import android.app.Activity; import android.app.AppOpsManager; import android.app.PendingIntent; @@ -42,6 +32,15 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; +import com.android.mms.service.exception.MmsHttpException; +import com.google.android.mms.MmsException; +import com.google.android.mms.pdu.GenericPdu; +import com.google.android.mms.pdu.PduHeaders; +import com.google.android.mms.pdu.PduParser; +import com.google.android.mms.pdu.PduPersister; +import com.google.android.mms.pdu.RetrieveConf; +import com.google.android.mms.util.SqliteWrapper; + import java.util.List; /** @@ -67,12 +66,19 @@ public class DownloadRequest extends MmsRequest { @Override protected byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettings apn) throws MmsHttpException { - return doHttpForResolvedAddresses(context, - netMgr, + final MmsHttpClient mmsHttpClient = netMgr.getOrCreateHttpClient(); + if (mmsHttpClient == null) { + Log.e(MmsService.TAG, "MMS network is not ready!"); + throw new MmsHttpException(0/*statusCode*/, "MMS network is not ready"); + } + return mmsHttpClient.execute( mLocationUrl, - null/*pdu*/, - HttpUtils.HTTP_GET_METHOD, - apn); + null/*pud*/, + MmsHttpClient.METHOD_GET, + apn.isProxySet(), + apn.getProxyAddress(), + apn.getProxyPort(), + mMmsConfig); } @Override diff --git a/src/com/android/mms/service/HttpUtils.java b/src/com/android/mms/service/HttpUtils.java deleted file mode 100644 index a58af39..0000000 --- a/src/com/android/mms/service/HttpUtils.java +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright (C) 2014 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 com.android.mms.service; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; - -import com.android.mms.service.exception.MmsHttpException; -import com.android.mms.service.http.NameResolver; -import com.android.mms.service.http.NetworkAwareHttpClient; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.params.ConnRouteParams; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.params.HttpProtocolParams; - -import java.io.DataInputStream; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * HTTP utils to make HTTP request to MMSC - */ -public class HttpUtils { - private static final String TAG = MmsService.TAG; - - public static final int HTTP_POST_METHOD = 1; - public static final int HTTP_GET_METHOD = 2; - - // Definition for necessary HTTP headers. - private static final String HDR_KEY_ACCEPT = "Accept"; - private static final String HDR_KEY_ACCEPT_LANGUAGE = "Accept-Language"; - - private static final String HDR_VALUE_ACCEPT = - "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"; - - private HttpUtils() { - // To forbidden instantiate this class. - } - - /** - * A helper method to send or retrieve data through HTTP protocol. - * - * @param url The URL used in a GET request. Null when the method is - * HTTP_POST_METHOD. - * @param pdu The data to be POST. Null when the method is HTTP_GET_METHOD. - * @param method HTTP_POST_METHOD or HTTP_GET_METHOD. - * @param isProxySet If proxy is set - * @param proxyHost The host of the proxy - * @param proxyPort The port of the proxy - * @param resolver The custom name resolver to use - * @param useIpv6 If we should use IPv6 address when the HTTP client resolves the host name - * @param mmsConfig The MmsConfig to use - * @return A byte array which contains the response data. - * If an HTTP error code is returned, an IOException will be thrown. - * @throws com.android.mms.service.exception.MmsHttpException if HTTP request gets error response (>=400) - */ - public static byte[] httpConnection(Context context, String url, byte[] pdu, int method, - boolean isProxySet, String proxyHost, int proxyPort, NameResolver resolver, - boolean useIpv6, MmsConfig.Overridden mmsConfig) throws MmsHttpException { - final String methodString = getMethodString(method); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "HttpUtils: request param list\n" - + "url=" + url + "\n" - + "method=" + methodString + "\n" - + "isProxySet=" + isProxySet + "\n" - + "proxyHost=" + proxyHost + "\n" - + "proxyPort=" + proxyPort + "\n" - + "size=" + (pdu != null ? pdu.length : 0)); - } else { - Log.d(TAG, "HttpUtils: " + methodString + " " + url); - } - - NetworkAwareHttpClient client = null; - try { - // Make sure to use a proxy which supports CONNECT. - URI hostUrl = new URI(url); - HttpHost target = new HttpHost(hostUrl.getHost(), hostUrl.getPort(), - HttpHost.DEFAULT_SCHEME_NAME); - client = createHttpClient(context, resolver, useIpv6, mmsConfig); - HttpRequest req = null; - - switch (method) { - case HTTP_POST_METHOD: - ByteArrayEntity entity = new ByteArrayEntity(pdu); - // Set request content type. - entity.setContentType("application/vnd.wap.mms-message"); - HttpPost post = new HttpPost(url); - post.setEntity(entity); - req = post; - break; - case HTTP_GET_METHOD: - req = new HttpGet(url); - break; - } - - // Set route parameters for the request. - HttpParams params = client.getParams(); - if (isProxySet) { - ConnRouteParams.setDefaultProxy(params, new HttpHost(proxyHost, proxyPort)); - } - req.setParams(params); - - // Set necessary HTTP headers for MMS transmission. - req.addHeader(HDR_KEY_ACCEPT, HDR_VALUE_ACCEPT); - - // UA Profile URL header - String xWapProfileTagName = mmsConfig.getUaProfTagName(); - String xWapProfileUrl = mmsConfig.getUaProfUrl(); - if (xWapProfileUrl != null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "HttpUtils: xWapProfUrl=" + xWapProfileUrl); - } - req.addHeader(xWapProfileTagName, xWapProfileUrl); - } - - // Extra http parameters. Split by '|' to get a list of value pairs. - // Separate each pair by the first occurrence of ':' to obtain a name and - // value. Replace the occurrence of the string returned by - // MmsConfig.getHttpParamsLine1Key() with the users telephone number inside - // the value. And replace the occurrence of the string returned by - // MmsConfig.getHttpParamsNaiKey() with the users NAI(Network Access Identifier) - // inside the value. - String extraHttpParams = mmsConfig.getHttpParams(); - - if (!TextUtils.isEmpty(extraHttpParams)) { - // Parse the parameter list - String paramList[] = extraHttpParams.split("\\|"); - for (String paramPair : paramList) { - String splitPair[] = paramPair.split(":", 2); - if (splitPair.length == 2) { - final String name = splitPair[0].trim(); - final String value = resolveMacro(context, splitPair[1].trim(), mmsConfig); - if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) { - req.addHeader(name, value); - } - } - } - } - req.addHeader(HDR_KEY_ACCEPT_LANGUAGE, getCurrentAcceptLanguage(Locale.getDefault())); - - final HttpResponse response = client.execute(target, req); - final StatusLine status = response.getStatusLine(); - final HttpEntity entity = response.getEntity(); - Log.d(TAG, "HttpUtils: status=" + status + " size=" - + (entity != null ? entity.getContentLength() : -1)); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - for (Header header : req.getAllHeaders()) { - if (header != null) { - Log.v(TAG, "HttpUtils: header " - + header.getName() + "=" + header.getValue()); - } - } - } - byte[] body = null; - if (entity != null) { - try { - if (entity.getContentLength() > 0) { - body = new byte[(int) entity.getContentLength()]; - DataInputStream dis = new DataInputStream(entity.getContent()); - try { - dis.readFully(body); - } finally { - try { - dis.close(); - } catch (IOException e) { - Log.e(TAG, "HttpUtils: Error closing input stream: " - + e.getMessage()); - } - } - } - if (entity.isChunked()) { - Log.d(TAG, "HttpUtils: transfer encoding is chunked"); - int bytesTobeRead = mmsConfig.getMaxMessageSize(); - byte[] tempBody = new byte[bytesTobeRead]; - DataInputStream dis = new DataInputStream(entity.getContent()); - try { - int bytesRead = 0; - int offset = 0; - boolean readError = false; - do { - try { - bytesRead = dis.read(tempBody, offset, bytesTobeRead); - } catch (IOException e) { - readError = true; - Log.e(TAG, "HttpUtils: error reading input stream", e); - break; - } - if (bytesRead > 0) { - bytesTobeRead -= bytesRead; - offset += bytesRead; - } - } while (bytesRead >= 0 && bytesTobeRead > 0); - if (bytesRead == -1 && offset > 0 && !readError) { - // offset is same as total number of bytes read - // bytesRead will be -1 if the data was read till the eof - body = new byte[offset]; - System.arraycopy(tempBody, 0, body, 0, offset); - Log.d(TAG, "HttpUtils: Chunked response length " + offset); - } else { - Log.e(TAG, "HttpUtils: Response entity too large or empty"); - } - } finally { - try { - dis.close(); - } catch (IOException e) { - Log.e(TAG, "HttpUtils: Error closing input stream", e); - } - } - } - } finally { - if (entity != null) { - entity.consumeContent(); - } - } - } - final int statusCode = status.getStatusCode(); - if (statusCode != 200) { // HTTP 200 is success. - StringBuilder sb = new StringBuilder(); - if (body != null) { - sb.append("response: text=").append(new String(body)).append('\n'); - } - for (Header header : req.getAllHeaders()) { - if (header != null) { - sb.append("req header: ") - .append(header.getName()) - .append('=') - .append(header.getValue()) - .append('\n'); - } - } - for (Header header : response.getAllHeaders()) { - if (header != null) { - sb.append("resp header: ") - .append(header.getName()) - .append('=') - .append(header.getValue()) - .append('\n'); - } - } - Log.e(TAG, "HttpUtils: error response -- \n" - + "mStatusCode=" + statusCode + "\n" - + "reason=" + status.getReasonPhrase() + "\n" - + "url=" + url + "\n" - + "method=" + methodString + "\n" - + "isProxySet=" + isProxySet + "\n" - + "proxyHost=" + proxyHost + "\n" - + "proxyPort=" + proxyPort - + (sb != null ? "\n" + sb.toString() : "")); - throw new MmsHttpException(statusCode, status.getReasonPhrase()); - } - return body; - } catch (IOException e) { - Log.e(TAG, "HttpUtils: IO failure", e); - throw new MmsHttpException(0/*statusCode*/, e); - } catch (URISyntaxException e) { - Log.e(TAG, "HttpUtils: invalid url " + url); - throw new MmsHttpException(0/*statusCode*/, "Invalid url " + url); - } finally { - if (client != null) { - client.close(); - } - } - } - - private static String getMethodString(int method) { - return ((method == HTTP_POST_METHOD) ? - "POST" : ((method == HTTP_GET_METHOD) ? "GET" : "UNKNOWN")); - } - - /** - * Create an HTTP client - * - * @param context - * @return {@link android.net.http.AndroidHttpClient} - */ - private static NetworkAwareHttpClient createHttpClient(Context context, NameResolver resolver, - boolean useIpv6, MmsConfig.Overridden mmsConfig) { - final String userAgent = mmsConfig.getUserAgent(); - final NetworkAwareHttpClient client = NetworkAwareHttpClient.newInstance(userAgent, context, - resolver, useIpv6); - final HttpParams params = client.getParams(); - HttpProtocolParams.setContentCharset(params, "UTF-8"); - - // set the socket timeout - int soTimeout = mmsConfig.getHttpSocketTimeout(); - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "HttpUtils: createHttpClient w/ socket timeout " - + soTimeout + " ms, UA=" + userAgent); - } - HttpConnectionParams.setSoTimeout(params, soTimeout); - return client; - } - - private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US"; - - /** - * Return the Accept-Language header. Use the current locale plus - * US if we are in a different locale than US. - * This code copied from the browser's WebSettings.java - * - * @return Current AcceptLanguage String. - */ - public static String getCurrentAcceptLanguage(Locale locale) { - final StringBuilder buffer = new StringBuilder(); - addLocaleToHttpAcceptLanguage(buffer, locale); - - if (!Locale.US.equals(locale)) { - if (buffer.length() > 0) { - buffer.append(", "); - } - buffer.append(ACCEPT_LANG_FOR_US_LOCALE); - } - - return buffer.toString(); - } - - /** - * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish, - * to new standard. - */ - private static String convertObsoleteLanguageCodeToNew(String langCode) { - if (langCode == null) { - return null; - } - if ("iw".equals(langCode)) { - // Hebrew - return "he"; - } else if ("in".equals(langCode)) { - // Indonesian - return "id"; - } else if ("ji".equals(langCode)) { - // Yiddish - return "yi"; - } - return langCode; - } - - private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale) { - final String language = convertObsoleteLanguageCodeToNew(locale.getLanguage()); - if (language != null) { - builder.append(language); - final String country = locale.getCountry(); - if (country != null) { - builder.append("-"); - builder.append(country); - } - } - } - - private static final Pattern MACRO_P = Pattern.compile("##(\\S+)##"); - /** - * Resolve the macro in HTTP param value text - * For example, "something##LINE1##something" is resolved to "something9139531419something" - * - * @param value The HTTP param value possibly containing macros - * @return The HTTP param with macro resolved to real value - */ - private static String resolveMacro(Context context, String value, - MmsConfig.Overridden mmsConfig) { - if (TextUtils.isEmpty(value)) { - return value; - } - final Matcher matcher = MACRO_P.matcher(value); - int nextStart = 0; - StringBuilder replaced = null; - while (matcher.find()) { - if (replaced == null) { - replaced = new StringBuilder(); - } - final int matchedStart = matcher.start(); - if (matchedStart > nextStart) { - replaced.append(value.substring(nextStart, matchedStart)); - } - final String macro = matcher.group(1); - final String macroValue = mmsConfig.getHttpParamMacro(context, macro); - if (macroValue != null) { - replaced.append(macroValue); - } else { - Log.w(TAG, "HttpUtils: invalid macro " + macro); - } - nextStart = matcher.end(); - } - if (replaced != null && nextStart < value.length()) { - replaced.append(value.substring(nextStart)); - } - return replaced == null ? value : replaced.toString(); - } -} diff --git a/src/com/android/mms/service/MmsHttpClient.java b/src/com/android/mms/service/MmsHttpClient.java new file mode 100644 index 0000000..8e1b4f1 --- /dev/null +++ b/src/com/android/mms/service/MmsHttpClient.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2014 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 com.android.mms.service; + +import javax.net.SocketFactory; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.android.mms.service.exception.MmsHttpException; +import com.android.okhttp.ConnectionPool; +import com.android.okhttp.HostResolver; +import com.android.okhttp.HttpHandler; +import com.android.okhttp.HttpsHandler; +import com.android.okhttp.OkHttpClient; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.URL; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * MMS HTTP client for sending and downloading MMS messages + */ +public class MmsHttpClient { + public static final String METHOD_POST = "POST"; + public static final String METHOD_GET = "GET"; + + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + private static final String HEADER_ACCEPT = "Accept"; + private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language"; + private static final String HEADER_USER_AGENT = "User-Agent"; + + // The "Accept" header value + private static final String HEADER_VALUE_ACCEPT = + "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"; + // The "Content-Type" header value + private static final String HEADER_VALUE_CONTENT_TYPE = + "application/vnd.wap.mms-message; charset=utf-8"; + + private final Context mContext; + private final SocketFactory mSocketFactory; + private final HostResolver mHostResolver; + private final ConnectionPool mConnectionPool; + + /** + * Constructor + * + * @param context The Context object + * @param socketFactory The socket factory for creating an OKHttp client + * @param hostResolver The host name resolver for creating an OKHttp client + * @param connectionPool The connection pool for creating an OKHttp client + */ + public MmsHttpClient(Context context, SocketFactory socketFactory, HostResolver hostResolver, + ConnectionPool connectionPool) { + mContext = context; + mSocketFactory = socketFactory; + mHostResolver = hostResolver; + mConnectionPool = connectionPool; + } + + /** + * Execute an MMS HTTP request, either a POST (sending) or a GET (downloading) + * + * @param urlString The request URL, for sending it is usually the MMSC, and for downloading + * it is the message URL + * @param pdu For POST (sending) only, the PDU to send + * @param method HTTP method, POST for sending and GET for downloading + * @param isProxySet Is there a proxy for the MMSC + * @param proxyHost The proxy host + * @param proxyPort The proxy port + * @param mmsConfig The MMS config to use + * @return The HTTP response body + * @throws MmsHttpException For any failures + */ + public byte[] execute(String urlString, byte[] pdu, String method, boolean isProxySet, + String proxyHost, int proxyPort, MmsConfig.Overridden mmsConfig) + throws MmsHttpException { + Log.d(MmsService.TAG, "HTTP: " + method + " " + urlString + + (isProxySet ? (", proxy=" + proxyHost + ":" + proxyPort) : "") + + ", PDU size=" + (pdu != null ? pdu.length : 0)); + checkMethod(method); + HttpURLConnection connection = null; + try { + Proxy proxy = null; + if (isProxySet) { + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + } + final URL url = new URL(urlString); + // Now get the connection + connection = openConnection(url, proxy); + connection.setDoInput(true); + connection.setConnectTimeout(mmsConfig.getHttpSocketTimeout()); + // ------- COMMON HEADERS --------- + // Header: Accept + connection.setRequestProperty(HEADER_ACCEPT, HEADER_VALUE_ACCEPT); + // Header: Accept-Language + connection.setRequestProperty( + HEADER_ACCEPT_LANGUAGE, getCurrentAcceptLanguage(Locale.getDefault())); + // Header: User-Agent + connection.setRequestProperty(HEADER_USER_AGENT, mmsConfig.getUserAgent()); + // Header: x-wap-profile + final String uaProfUrlTagName = mmsConfig.getUaProfTagName(); + final String uaProfUrl = mmsConfig.getUaProfUrl(); + if (uaProfUrl != null) { + if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { + Log.v(MmsService.TAG, "HTTP: UaProfUrl=" + uaProfUrl); + } + connection.setRequestProperty(uaProfUrlTagName, uaProfUrl); + } + // Add extra headers specified by mms_config.xml's httpparams + addExtraHeaders(connection, mmsConfig); + // Different stuff for GET and POST + if (METHOD_POST.equals(method)) { + if (pdu == null || pdu.length < 1) { + Log.e(MmsService.TAG, "HTTP: empty pdu"); + throw new MmsHttpException(0/*statusCode*/, "Sending empty PDU"); + } + connection.setDoOutput(true); + connection.setRequestMethod(METHOD_POST); + connection.setRequestProperty(HEADER_CONTENT_TYPE, HEADER_VALUE_CONTENT_TYPE); + if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { + logHttpHeaders(connection.getRequestProperties()); + } + connection.setFixedLengthStreamingMode(pdu.length); + // Sending request body + final OutputStream out = + new BufferedOutputStream(connection.getOutputStream()); + out.write(pdu); + out.flush(); + out.close(); + } else if (METHOD_GET.equals(method)) { + if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { + logHttpHeaders(connection.getRequestProperties()); + } + connection.setRequestMethod(METHOD_GET); + } + // Get response + final InputStream in = new BufferedInputStream(connection.getInputStream()); + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + final byte[] buf = new byte[4096]; + int count = 0; + while ((count = in.read(buf)) > 0) { + byteOut.write(buf, 0, count); + } + in.close(); + final byte[] responseBody = byteOut.toByteArray(); + final int responseCode = connection.getResponseCode(); + final String responseMessage = connection.getResponseMessage(); + Log.d(MmsService.TAG, "HTTP: " + responseCode + " " + responseMessage); + if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) { + logHttpHeaders(connection.getHeaderFields()); + } + if (responseCode / 100 != 2) { + Log.e(MmsService.TAG, "HTTP: response=" + + (responseBody != null ? " " + new String(responseBody, "UTF=8") : "")); + throw new MmsHttpException(responseCode, responseMessage); + } + Log.d(MmsService.TAG, "HTTP: response size=" + + (responseBody != null ? responseBody.length : 0)); + return responseBody; + } catch (MalformedURLException e) { + Log.e(MmsService.TAG, "HTTP: invalid URL " + urlString, e); + throw new MmsHttpException(0/*statusCode*/, "Invalid URL " + urlString, e); + } catch (ProtocolException e) { + Log.e(MmsService.TAG, "HTTP: invalid URL protocol " + urlString, e); + throw new MmsHttpException(0/*statusCode*/, "Invalid URL protocol " + urlString, e); + } catch (IOException e) { + Log.e(MmsService.TAG, "HTTP: IO failure", e); + throw new MmsHttpException(0/*statusCode*/, e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + /** + * Open an HTTP connection + * + * TODO: The following code is borrowed from android.net.Network.openConnection + * Once that method supports proxy, we should use that instead + * Also we should remove the associated HostResolver and ConnectionPool from + * MmsNetworkManager + * + * @param url The URL to connect to + * @param proxy The proxy to use + * @return The opened HttpURLConnection + * @throws MalformedURLException If URL is malformed + */ + private HttpURLConnection openConnection(URL url, Proxy proxy) throws MalformedURLException { + final String protocol = url.getProtocol(); + OkHttpClient okHttpClient; + if (protocol.equals("http")) { + okHttpClient = HttpHandler.createHttpOkHttpClient(proxy); + } else if (protocol.equals("https")) { + okHttpClient = HttpsHandler.createHttpsOkHttpClient(proxy); + } else { + throw new MalformedURLException("Invalid URL or unrecognized protocol " + protocol); + } + return okHttpClient.setSocketFactory(mSocketFactory) + .setHostResolver(mHostResolver) + .setConnectionPool(mConnectionPool) + .open(url); + } + + private static void logHttpHeaders(Map<String, List<String>> headers) { + final StringBuilder sb = new StringBuilder(); + if (headers != null) { + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { + final String key = entry.getKey(); + final List<String> values = entry.getValue(); + if (values != null) { + for (String value : values) { + sb.append(key).append('=').append(value).append('\n'); + } + } + } + Log.v(MmsService.TAG, "HTTP: headers\n" + sb.toString()); + } + } + + private static void checkMethod(String method) throws MmsHttpException { + if (!METHOD_GET.equals(method) && !METHOD_POST.equals(method)) { + throw new MmsHttpException(0/*statusCode*/, "Invalid method " + method); + } + } + + private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US"; + + /** + * Return the Accept-Language header. Use the current locale plus + * US if we are in a different locale than US. + * This code copied from the browser's WebSettings.java + * + * @return Current AcceptLanguage String. + */ + public static String getCurrentAcceptLanguage(Locale locale) { + final StringBuilder buffer = new StringBuilder(); + addLocaleToHttpAcceptLanguage(buffer, locale); + + if (!Locale.US.equals(locale)) { + if (buffer.length() > 0) { + buffer.append(", "); + } + buffer.append(ACCEPT_LANG_FOR_US_LOCALE); + } + + return buffer.toString(); + } + + /** + * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish, + * to new standard. + */ + private static String convertObsoleteLanguageCodeToNew(String langCode) { + if (langCode == null) { + return null; + } + if ("iw".equals(langCode)) { + // Hebrew + return "he"; + } else if ("in".equals(langCode)) { + // Indonesian + return "id"; + } else if ("ji".equals(langCode)) { + // Yiddish + return "yi"; + } + return langCode; + } + + private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale) { + final String language = convertObsoleteLanguageCodeToNew(locale.getLanguage()); + if (language != null) { + builder.append(language); + final String country = locale.getCountry(); + if (country != null) { + builder.append("-"); + builder.append(country); + } + } + } + + private static final Pattern MACRO_P = Pattern.compile("##(\\S+)##"); + /** + * Resolve the macro in HTTP param value text + * For example, "something##LINE1##something" is resolved to "something9139531419something" + * + * @param value The HTTP param value possibly containing macros + * @return The HTTP param with macro resolved to real value + */ + private static String resolveMacro(Context context, String value, + MmsConfig.Overridden mmsConfig) { + if (TextUtils.isEmpty(value)) { + return value; + } + final Matcher matcher = MACRO_P.matcher(value); + int nextStart = 0; + StringBuilder replaced = null; + while (matcher.find()) { + if (replaced == null) { + replaced = new StringBuilder(); + } + final int matchedStart = matcher.start(); + if (matchedStart > nextStart) { + replaced.append(value.substring(nextStart, matchedStart)); + } + final String macro = matcher.group(1); + final String macroValue = mmsConfig.getHttpParamMacro(context, macro); + if (macroValue != null) { + replaced.append(macroValue); + } else { + Log.w(MmsService.TAG, "HTTP: invalid macro " + macro); + } + nextStart = matcher.end(); + } + if (replaced != null && nextStart < value.length()) { + replaced.append(value.substring(nextStart)); + } + return replaced == null ? value : replaced.toString(); + } + + /** + * Add extra HTTP headers from mms_config.xml's httpParams, which is a list of key/value + * pairs separated by "|". Each key/value pair is separated by ":". Value may contain + * macros like "##LINE1##" or "##NAI##" which is resolved with methods in this class + * + * @param connection The HttpURLConnection that we add headers to + * @param mmsConfig The MmsConfig object + */ + private void addExtraHeaders(HttpURLConnection connection, MmsConfig.Overridden mmsConfig) { + final String extraHttpParams = mmsConfig.getHttpParams(); + if (!TextUtils.isEmpty(extraHttpParams)) { + // Parse the parameter list + String paramList[] = extraHttpParams.split("\\|"); + for (String paramPair : paramList) { + String splitPair[] = paramPair.split(":", 2); + if (splitPair.length == 2) { + final String name = splitPair[0].trim(); + final String value = resolveMacro(mContext, splitPair[1].trim(), mmsConfig); + if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) { + // Add the header if the param is valid + connection.setRequestProperty(name, value); + } + } + } + } + } +} diff --git a/src/com/android/mms/service/MmsNetworkManager.java b/src/com/android/mms/service/MmsNetworkManager.java index cefafc7..a9c5eca 100644 --- a/src/com/android/mms/service/MmsNetworkManager.java +++ b/src/com/android/mms/service/MmsNetworkManager.java @@ -16,9 +16,6 @@ package com.android.mms.service; -import com.android.mms.service.exception.MmsNetworkException; -import com.android.mms.service.http.NameResolver; - import android.content.Context; import android.net.ConnectivityManager; import android.net.Network; @@ -28,13 +25,17 @@ import android.os.SystemClock; import android.provider.Settings; import android.util.Log; +import com.android.mms.service.exception.MmsNetworkException; +import com.android.okhttp.ConnectionPool; +import com.android.okhttp.HostResolver; + import java.net.InetAddress; import java.net.UnknownHostException; /** * Manages the MMS network connectivity */ -public class MmsNetworkManager implements NameResolver { +public class MmsNetworkManager implements HostResolver { // Timeout used to call ConnectivityManager.requestNetwork private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000; // Wait timeout for this class, a little bit longer than the above timeout @@ -42,6 +43,14 @@ public class MmsNetworkManager implements NameResolver { private static final int NETWORK_ACQUIRE_TIMEOUT_MILLIS = NETWORK_REQUEST_TIMEOUT_MILLIS + (5 * 1000); + // Borrowed from {@link android.net.Network} + private static final boolean httpKeepAlive = + Boolean.parseBoolean(System.getProperty("http.keepAlive", "true")); + private static final int httpMaxConnections = + httpKeepAlive ? Integer.parseInt(System.getProperty("http.maxConnections", "5")) : 0; + private static final long httpKeepAliveDurationMs = + Long.parseLong(System.getProperty("http.keepAliveDuration", "300000")); // 5 minutes. + private Context mContext; // The requested MMS {@link android.net.Network} we are holding // We need this when we unbind from it. This is also used to indicate if the @@ -60,7 +69,13 @@ public class MmsNetworkManager implements NameResolver { // The callback to register when we request MMS network private ConnectivityManager.NetworkCallback mNetworkCallback; - private ConnectivityManager mConnectivityManager; + private volatile ConnectivityManager mConnectivityManager; + + // The OkHttp's ConnectionPool used by the HTTP client associated with this network manager + private ConnectionPool mConnectionPool; + + // The MMS HTTP client for this network + private MmsHttpClient mMmsHttpClient; // TODO: we need to re-architect this when we support MSIM, like maybe one manager for each SIM? public MmsNetworkManager(Context context) { @@ -69,12 +84,8 @@ public class MmsNetworkManager implements NameResolver { mNetwork = null; mMmsRequestCount = 0; mConnectivityManager = null; - } - - public Network getNetwork() { - synchronized (this) { - return mNetwork; - } + mConnectionPool = null; + mMmsHttpClient = null; } /** @@ -201,6 +212,12 @@ public class MmsNetworkManager implements NameResolver { mNetworkCallback = null; mNetwork = null; mMmsRequestCount = 0; + // Currently we follow what android.net.Network does with ConnectionPool, + // which is per Network object. So if Network changes, we should clear + // out the ConnectionPool and thus the MmsHttpClient (since it is linked + // to a specific ConnectionPool). + mConnectionPool = null; + mMmsHttpClient = null; } @Override @@ -225,4 +242,32 @@ public class MmsNetworkManager implements NameResolver { return Settings.System.getInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0; } + + private ConnectionPool getOrCreateConnectionPoolLocked() { + if (mConnectionPool == null) { + mConnectionPool = new ConnectionPool(httpMaxConnections, httpKeepAliveDurationMs); + } + return mConnectionPool; + } + + /** + * Get an MmsHttpClient for the current network + * + * @return The MmsHttpClient instance + */ + public MmsHttpClient getOrCreateHttpClient() { + synchronized (this) { + if (mMmsHttpClient == null) { + if (mNetwork != null) { + // Create new MmsHttpClient for the current Network + mMmsHttpClient = new MmsHttpClient( + mContext, + mNetwork.getSocketFactory(), + MmsNetworkManager.this, + getOrCreateConnectionPoolLocked()); + } + } + return mMmsHttpClient; + } + } } diff --git a/src/com/android/mms/service/MmsRequest.java b/src/com/android/mms/service/MmsRequest.java index 04b9ff9..adaf083 100644 --- a/src/com/android/mms/service/MmsRequest.java +++ b/src/com/android/mms/service/MmsRequest.java @@ -16,30 +16,20 @@ package com.android.mms.service; -import com.android.mms.service.exception.ApnException; -import com.android.mms.service.exception.MmsHttpException; -import com.android.mms.service.exception.MmsNetworkException; - import android.app.Activity; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.LinkProperties; -import android.net.Network; import android.net.Uri; import android.os.Bundle; import android.provider.Telephony; import android.telephony.SmsManager; import android.util.Log; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; +import com.android.mms.service.exception.ApnException; +import com.android.mms.service.exception.MmsHttpException; +import com.android.mms.service.exception.MmsNetworkException; /** * Base class for MMS requests. This has the common logic of sending/downloading MMS. @@ -216,162 +206,6 @@ public abstract class MmsRequest { } /** - * Try running MMS HTTP request for all the addresses that we can resolve to - * - * @param context The context - * @param netMgr The {@link com.android.mms.service.MmsNetworkManager} - * @param url The HTTP URL - * @param pdu The PDU to send - * @param method The HTTP method to use - * @param apn The APN setting to use - * @return The response data - * @throws MmsHttpException If there is any HTTP/network failure - */ - protected byte[] doHttpForResolvedAddresses(Context context, MmsNetworkManager netMgr, - String url, byte[] pdu, int method, ApnSettings apn) throws MmsHttpException { - MmsHttpException lastException = null; - final ConnectivityManager connMgr = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - // Do HTTP on all the addresses we can resolve to - for (final InetAddress address : resolveDestination(connMgr, netMgr, url, apn)) { - try { - // TODO: we have to use a deprecated API here because with the new - // ConnectivityManager APIs in LMP, we need to either use a bound process - // or a bound socket. The former can not be used since we share the - // phone process with others. The latter is not supported by any HTTP - // library yet. We have to rely on this API to get things work. Once - // a multinet aware HTTP lib is ready, we should switch to that and - // remove all the unnecessary code. - if (!connMgr.requestRouteToHostAddress( - ConnectivityManager.TYPE_MOBILE_MMS, address)) { - throw new MmsHttpException(0/*statusCode*/, - "MmsRequest: can not request a route for host " + address); - } - return HttpUtils.httpConnection( - context, - url, - pdu, - method, - apn.isProxySet(), - apn.getProxyAddress(), - apn.getProxyPort(), - netMgr, - address instanceof Inet6Address, - mMmsConfig); - } catch (MmsHttpException e) { - lastException = e; - Log.e(MmsService.TAG, "MmsRequest: failure in trying address " + address, e); - } - } - if (lastException != null) { - throw lastException; - } else { - // Should not reach here - throw new MmsHttpException(0/*statusCode*/, "MmsRequest: unknown failure"); - } - } - - /** - * Resolve the name of the host we are about to connect to, which can be the URL host or - * the proxy host. We only resolve to the supported address types (IPv4 or IPv6 or both) - * based on the MMS network interface's address type, i.e. we only need addresses that - * match the link address type. - * - * @param connMgr The connectivity manager - * @param netMgr The current {@link MmsNetworkManager} - * @param url The HTTP URL - * @param apn The APN setting to use - * @return A list of matching resolved addresses - * @throws MmsHttpException For any network failure - */ - private static List<InetAddress> resolveDestination(ConnectivityManager connMgr, - MmsNetworkManager netMgr, String url, ApnSettings apn) throws MmsHttpException { - Log.d(MmsService.TAG, "MmsRequest: resolve url " + url); - // Find the real host to connect to - String host = null; - if (apn.isProxySet()) { - host = apn.getProxyAddress(); - } else { - final Uri uri = Uri.parse(url); - host = uri.getHost(); - } - // Find out the link address types: ipv4 or ipv6 or both - final int addressTypes = getMmsLinkAddressTypes(connMgr, netMgr.getNetwork()); - Log.d(MmsService.TAG, "MmsRequest: addressTypes=" + addressTypes); - // Resolve the host to a list of addresses based on supported address types - return resolveHostName(netMgr, host, addressTypes); - } - - // Address type masks - private static final int ADDRESS_TYPE_IPV4 = 1; - private static final int ADDRESS_TYPE_IPV6 = 1 << 1; - - /** - * Try to find out if we should use IPv6 or IPv4 for MMS. Basically we check if the MMS - * network interface has IPv6 address or not. If so, we will use IPv6. Otherwise, use - * IPv4. - * - * @param connMgr The connectivity manager - * @return A bit mask indicating what address types we have - */ - private static int getMmsLinkAddressTypes(ConnectivityManager connMgr, Network network) { - int result = 0; - // Return none if network is not available - if (network == null) { - return result; - } - final LinkProperties linkProperties = connMgr.getLinkProperties(network); - if (linkProperties != null) { - for (InetAddress addr : linkProperties.getAddresses()) { - if (addr instanceof Inet4Address) { - result |= ADDRESS_TYPE_IPV4; - } else if (addr instanceof Inet6Address) { - result |= ADDRESS_TYPE_IPV6; - } - } - } - return result; - } - - /** - * Resolve host name to address by specified address types. - * - * @param netMgr The current {@link MmsNetworkManager} - * @param host The host name - * @param addressTypes The required address type in a bit mask - * (0x01: IPv4, 0x10: IPv6, 0x11: both) - * @return - * @throws MmsHttpException - */ - private static List<InetAddress> resolveHostName(MmsNetworkManager netMgr, String host, - int addressTypes) throws MmsHttpException { - final List<InetAddress> resolved = new ArrayList<InetAddress>(); - try { - if (addressTypes != 0) { - for (final InetAddress addr : netMgr.getAllByName(host)) { - if ((addressTypes & ADDRESS_TYPE_IPV6) != 0 - && addr instanceof Inet6Address) { - // Should use IPv6 and this is IPv6 address, add it - resolved.add(addr); - } else if ((addressTypes & ADDRESS_TYPE_IPV4) != 0 - && addr instanceof Inet4Address) { - // Should use IPv4 and this is IPv4 address, add it - resolved.add(addr); - } - } - } - if (resolved.size() < 1) { - throw new MmsHttpException(0/*statusCode*/, - "Failed to resolve " + host - + " for allowed address types: " + addressTypes); - } - return resolved; - } catch (final UnknownHostException e) { - throw new MmsHttpException(0/*statusCode*/, "Failed to resolve " + host, e); - } - } - - /** * Process the result of the completed request, including updating the message status * in database and sending back the result via pending intents. * @param context The context diff --git a/src/com/android/mms/service/SendRequest.java b/src/com/android/mms/service/SendRequest.java index 6d016ae..64c47f1 100644 --- a/src/com/android/mms/service/SendRequest.java +++ b/src/com/android/mms/service/SendRequest.java @@ -16,16 +16,6 @@ package com.android.mms.service; -import com.google.android.mms.MmsException; -import com.google.android.mms.pdu.GenericPdu; -import com.google.android.mms.pdu.PduParser; -import com.google.android.mms.pdu.PduPersister; -import com.google.android.mms.pdu.SendConf; -import com.google.android.mms.pdu.SendReq; -import com.google.android.mms.util.SqliteWrapper; - -import com.android.mms.service.exception.MmsHttpException; - import android.app.Activity; import android.app.AppOpsManager; import android.app.PendingIntent; @@ -43,6 +33,15 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; +import com.android.mms.service.exception.MmsHttpException; +import com.google.android.mms.MmsException; +import com.google.android.mms.pdu.GenericPdu; +import com.google.android.mms.pdu.PduParser; +import com.google.android.mms.pdu.PduPersister; +import com.google.android.mms.pdu.SendConf; +import com.google.android.mms.pdu.SendReq; +import com.google.android.mms.util.SqliteWrapper; + import java.util.List; /** @@ -67,12 +66,19 @@ public class SendRequest extends MmsRequest { @Override protected byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettings apn) throws MmsHttpException { - return doHttpForResolvedAddresses(context, - netMgr, + final MmsHttpClient mmsHttpClient = netMgr.getOrCreateHttpClient(); + if (mmsHttpClient == null) { + Log.e(MmsService.TAG, "MMS network is not ready!"); + throw new MmsHttpException(0/*statusCode*/, "MMS network is not ready"); + } + return mmsHttpClient.execute( mLocationUrl != null ? mLocationUrl : apn.getMmscUrl(), mPduData, - HttpUtils.HTTP_POST_METHOD, - apn); + MmsHttpClient.METHOD_POST, + apn.isProxySet(), + apn.getProxyAddress(), + apn.getProxyPort(), + mMmsConfig); } @Override diff --git a/src/com/android/mms/service/http/NameResolver.java b/src/com/android/mms/service/http/NameResolver.java deleted file mode 100644 index 13b4046..0000000 --- a/src/com/android/mms/service/http/NameResolver.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2014 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 com.android.mms.service.http; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -/** - * An interface for DNS name resolver - */ -public interface NameResolver { - public InetAddress[] getAllByName(String host) throws UnknownHostException; -} diff --git a/src/com/android/mms/service/http/NetworkAwareClientConnectionOperator.java b/src/com/android/mms/service/http/NetworkAwareClientConnectionOperator.java deleted file mode 100644 index ec5fa59..0000000 --- a/src/com/android/mms/service/http/NetworkAwareClientConnectionOperator.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2014 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 com.android.mms.service.http; - -import com.android.mms.service.MmsService; - -import org.apache.http.HttpHost; -import org.apache.http.conn.ConnectTimeoutException; -import org.apache.http.conn.HttpHostConnectException; -import org.apache.http.conn.OperatedClientConnection; -import org.apache.http.conn.scheme.LayeredSocketFactory; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.scheme.SocketFactory; -import org.apache.http.impl.conn.DefaultClientConnectionOperator; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.HttpContext; - -import android.util.Log; - -import java.io.IOException; -import java.net.ConnectException; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.Socket; -import java.net.SocketException; -import java.util.ArrayList; - -/** - * This is a subclass of {@link org.apache.http.impl.conn.DefaultClientConnectionOperator} - * which allows us to use a custom name resolver and pick the address type when we resolve - * the host name to connect. - */ -public class NetworkAwareClientConnectionOperator extends DefaultClientConnectionOperator { - private static final PlainSocketFactory staticPlainSocketFactory = new PlainSocketFactory(); - - private NameResolver mResolver; - private boolean mShouldUseIpv6; - - public NetworkAwareClientConnectionOperator(SchemeRegistry schemes) { - super(schemes); - } - - public void setNameResolver(NameResolver resolver) { - mResolver = resolver; - } - - public void setShouldUseIpv6(boolean value) { - mShouldUseIpv6 = value; - } - - /** - * Resolve name by address type. Only returns IPv6 addresses if required, or IPv4 if not. - * - * @param hostName - * @return The list addresses resolved - * @throws java.io.IOException - */ - private ArrayList<InetAddress> resolveHostName(final String hostName) throws IOException { - final ArrayList<InetAddress> addresses = new ArrayList<InetAddress>(); - for (final InetAddress address : mResolver.getAllByName(hostName)) { - if (mShouldUseIpv6 && address instanceof Inet6Address) { - addresses.add(address); - } else if (!mShouldUseIpv6 && address instanceof Inet4Address){ - addresses.add(address); - } - } - return addresses; - } - - /** - * This method is mostly copied from the overridden one in parent. The only change - * is how we resolve host name. - */ - @Override - public void openConnection(OperatedClientConnection conn, HttpHost target, InetAddress local, - HttpContext context, HttpParams params) throws IOException { - if (conn == null) { - throw new IllegalArgumentException - ("Connection must not be null."); - } - if (target == null) { - throw new IllegalArgumentException - ("Target host must not be null."); - } - // local address may be null - //@@@ is context allowed to be null? - if (params == null) { - throw new IllegalArgumentException - ("Parameters must not be null."); - } - if (conn.isOpen()) { - throw new IllegalArgumentException - ("Connection must not be open."); - } - - final Scheme schm = schemeRegistry.getScheme(target.getSchemeName()); - final SocketFactory sf = schm.getSocketFactory(); - final SocketFactory plain_sf; - final LayeredSocketFactory layered_sf; - if (sf instanceof LayeredSocketFactory) { - plain_sf = staticPlainSocketFactory; - layered_sf = (LayeredSocketFactory)sf; - } else { - plain_sf = sf; - layered_sf = null; - } - // CHANGE FOR MmsService - ArrayList<InetAddress> addresses = resolveHostName(target.getHostName()); - - for (int i = 0; i < addresses.size(); ++i) { - Log.d(MmsService.TAG, "NetworkAwareClientConnectionOperator: connecting " - + addresses.get(i)); - Socket sock = plain_sf.createSocket(); - conn.opening(sock, target); - - try { - Socket connsock = plain_sf.connectSocket(sock, - addresses.get(i).getHostAddress(), - schm.resolvePort(target.getPort()), - local, 0, params); - if (sock != connsock) { - sock = connsock; - conn.opening(sock, target); - } - /* - * prepareSocket is called on the just connected - * socket before the creation of the layered socket to - * ensure that desired socket options such as - * TCP_NODELAY, SO_RCVTIMEO, SO_LINGER will be set - * before any I/O is performed on the socket. This - * happens in the common case as - * SSLSocketFactory.createSocket performs hostname - * verification which requires that SSL handshaking be - * performed. - */ - prepareSocket(sock, context, params); - if (layered_sf != null) { - Socket layeredsock = layered_sf.createSocket(sock, - target.getHostName(), - schm.resolvePort(target.getPort()), - true); - if (layeredsock != sock) { - conn.opening(layeredsock, target); - } - conn.openCompleted(sf.isSecure(layeredsock), params); - } else { - conn.openCompleted(sf.isSecure(sock), params); - } - break; - // BEGIN android-changed - // catch SocketException to cover any kind of connect failure - } catch (SocketException ex) { - if (i == addresses.size() - 1) { - ConnectException cause = ex instanceof ConnectException - ? (ConnectException) ex : - (ConnectException) new ConnectException( - ex.getMessage()).initCause(ex); - throw new HttpHostConnectException(target, cause); - } - // END android-changed - } catch (ConnectTimeoutException ex) { - if (i == addresses.size() - 1) { - throw ex; - } - } - } - } -} diff --git a/src/com/android/mms/service/http/NetworkAwareHttpClient.java b/src/com/android/mms/service/http/NetworkAwareHttpClient.java deleted file mode 100644 index 646670d..0000000 --- a/src/com/android/mms/service/http/NetworkAwareHttpClient.java +++ /dev/null @@ -1,504 +0,0 @@ -/* - * Copyright (C) 2014 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 com.android.mms.service.http; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpException; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpRequestInterceptor; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.ResponseHandler; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.client.protocol.ClientContext; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.client.RequestWrapper; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.params.HttpProtocolParams; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.BasicHttpProcessor; -import org.apache.http.protocol.HttpContext; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.SSLCertificateSocketFactory; -import android.net.SSLSessionCache; -import android.os.Looper; -import android.util.Base64; -import android.util.Log; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** - * This is a copy of {@link android.net.http.AndroidHttpClient} with changes - * to allow us using a different {@org.apache.http.conn.ClientConnectionManager} - */ - -/** - * Implementation of the Apache {@link org.apache.http.impl.client.DefaultHttpClient} that is configured with - * reasonable default settings and registered schemes for Android. - * Don't create this directly, use the {@link #newInstance} factory method. - * - * <p>This client processes cookies but does not retain them by default. - * To retain cookies, simply add a cookie store to the HttpContext:</p> - * - * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre> - */ -public final class NetworkAwareHttpClient implements HttpClient { - - // Gzip of data shorter than this probably won't be worthwhile - public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256; - - // Default connection and socket timeout of 60 seconds. Tweak to taste. - private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000; - - private static final String TAG = "MmsHttpClient"; - - private static String[] textContentTypes = new String[] { - "text/", - "application/xml", - "application/json" - }; - - /** Interceptor throws an exception if the executing thread is blocked */ - private static final HttpRequestInterceptor sThreadCheckInterceptor = - new HttpRequestInterceptor() { - @Override - public void process(HttpRequest request, HttpContext context) { - // Prevent the HttpRequest from being sent on the main thread - if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) { - throw new RuntimeException("This thread forbids HTTP requests"); - } - } - }; - - /** - * Create a new HttpClient with reasonable defaults (which you can update). - * - * @param userAgent to report in your HTTP requests - * @param context to use for caching SSL sessions (may be null for no caching) - * @return AndroidHttpClient for you to use for all your requests. - */ - public static NetworkAwareHttpClient newInstance(String userAgent, Context context, - NameResolver resolver, boolean shouldUseIpv6) { - HttpParams params = new BasicHttpParams(); - - // Turn off stale checking. Our connections break all the time anyway, - // and it's not worth it to pay the penalty of checking every time. - HttpConnectionParams.setStaleCheckingEnabled(params, false); - - HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT); - HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT); - HttpConnectionParams.setSocketBufferSize(params, 8192); - - // Don't handle redirects -- return them to the caller. Our code - // often wants to re-POST after a redirect, which we must do ourselves. - HttpClientParams.setRedirecting(params, false); - - // Use a session cache for SSL sockets - SSLSessionCache sessionCache = context == null ? null : new SSLSessionCache(context); - - // Set the specified user agent and register standard protocols. - HttpProtocolParams.setUserAgent(params, userAgent); - SchemeRegistry schemeRegistry = new SchemeRegistry(); - schemeRegistry.register(new Scheme("http", - PlainSocketFactory.getSocketFactory(), 80)); - schemeRegistry.register(new Scheme("https", - SSLCertificateSocketFactory.getHttpSocketFactory( - SOCKET_OPERATION_TIMEOUT, sessionCache), 443)); - - /* - * CHANGE FOR MmsService: using a different ClientConnectionManager which - * uses a custom name resolver and can specify address type - */ - ClientConnectionManager manager = new NetworkAwareThreadSafeClientConnManager( - params, schemeRegistry, resolver, shouldUseIpv6); - - // We use a factory method to modify superclass initialization - // parameters without the funny call-a-static-method dance. - return new NetworkAwareHttpClient(manager, params); - } - - private final HttpClient delegate; - - private RuntimeException mLeakedException = new IllegalStateException( - "AndroidHttpClient created and never closed"); - - private NetworkAwareHttpClient(ClientConnectionManager ccm, HttpParams params) { - this.delegate = new DefaultHttpClient(ccm, params) { - @Override - protected BasicHttpProcessor createHttpProcessor() { - // Add interceptor to prevent making requests from main thread. - BasicHttpProcessor processor = super.createHttpProcessor(); - processor.addRequestInterceptor(sThreadCheckInterceptor); - processor.addRequestInterceptor(new CurlLogger()); - - return processor; - } - - @Override - protected HttpContext createHttpContext() { - // Same as DefaultHttpClient.createHttpContext() minus the - // cookie store. - HttpContext context = new BasicHttpContext(); - context.setAttribute( - ClientContext.AUTHSCHEME_REGISTRY, - getAuthSchemes()); - context.setAttribute( - ClientContext.COOKIESPEC_REGISTRY, - getCookieSpecs()); - context.setAttribute( - ClientContext.CREDS_PROVIDER, - getCredentialsProvider()); - return context; - } - }; - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - if (mLeakedException != null) { - Log.e(TAG, "Leak found", mLeakedException); - mLeakedException = null; - } - } - - /** - * Modifies a request to indicate to the server that we would like a - * gzipped response. (Uses the "Accept-Encoding" HTTP header.) - * @param request the request to modify - * @see #getUngzippedContent - */ - public static void modifyRequestToAcceptGzipResponse(HttpRequest request) { - request.addHeader("Accept-Encoding", "gzip"); - } - - /** - * Gets the input stream from a response entity. If the entity is gzipped - * then this will get a stream over the uncompressed data. - * - * @param entity the entity whose content should be read - * @return the input stream to read from - * @throws java.io.IOException - */ - public static InputStream getUngzippedContent(HttpEntity entity) - throws IOException { - InputStream responseStream = entity.getContent(); - if (responseStream == null) return responseStream; - Header header = entity.getContentEncoding(); - if (header == null) return responseStream; - String contentEncoding = header.getValue(); - if (contentEncoding == null) return responseStream; - if (contentEncoding.contains("gzip")) responseStream - = new GZIPInputStream(responseStream); - return responseStream; - } - - /** - * Release resources associated with this client. You must call this, - * or significant resources (sockets and memory) may be leaked. - */ - public void close() { - if (mLeakedException != null) { - getConnectionManager().shutdown(); - mLeakedException = null; - } - } - - @Override - public HttpParams getParams() { - return delegate.getParams(); - } - - @Override - public ClientConnectionManager getConnectionManager() { - return delegate.getConnectionManager(); - } - - @Override - public HttpResponse execute(HttpUriRequest request) throws IOException { - return delegate.execute(request); - } - - @Override - public HttpResponse execute(HttpUriRequest request, HttpContext context) - throws IOException { - return delegate.execute(request, context); - } - - @Override - public HttpResponse execute(HttpHost target, HttpRequest request) - throws IOException { - return delegate.execute(target, request); - } - - @Override - public HttpResponse execute(HttpHost target, HttpRequest request, - HttpContext context) throws IOException { - return delegate.execute(target, request, context); - } - - @Override - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler) - throws IOException, ClientProtocolException { - return delegate.execute(request, responseHandler); - } - - @Override - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return delegate.execute(request, responseHandler, context); - } - - @Override - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler) throws IOException, - ClientProtocolException { - return delegate.execute(target, request, responseHandler); - } - - @Override - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return delegate.execute(target, request, responseHandler, context); - } - - /** - * Compress data to send to server. - * Creates a Http Entity holding the gzipped data. - * The data will not be compressed if it is too short. - * @param data The bytes to compress - * @return Entity holding the data - */ - public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver) - throws IOException { - AbstractHttpEntity entity; - if (data.length < getMinGzipSize(resolver)) { - entity = new ByteArrayEntity(data); - } else { - ByteArrayOutputStream arr = new ByteArrayOutputStream(); - OutputStream zipper = new GZIPOutputStream(arr); - zipper.write(data); - zipper.close(); - entity = new ByteArrayEntity(arr.toByteArray()); - entity.setContentEncoding("gzip"); - } - return entity; - } - - /** - * Retrieves the minimum size for compressing data. - * Shorter data will not be compressed. - */ - public static long getMinGzipSize(ContentResolver resolver) { - return DEFAULT_SYNC_MIN_GZIP_BYTES; // For now, this is just a constant. - } - - /* cURL logging support. */ - - /** - * Logging tag and level. - */ - private static class LoggingConfiguration { - - private final String tag; - private final int level; - - private LoggingConfiguration(String tag, int level) { - this.tag = tag; - this.level = level; - } - - /** - * Returns true if logging is turned on for this configuration. - */ - private boolean isLoggable() { - return Log.isLoggable(tag, level); - } - - /** - * Prints a message using this configuration. - */ - private void println(String message) { - Log.println(level, tag, message); - } - } - - /** cURL logging configuration. */ - private volatile LoggingConfiguration curlConfiguration; - - /** - * Enables cURL request logging for this client. - * - * @param name to log messages with - * @param level at which to log messages (see {@link android.util.Log}) - */ - public void enableCurlLogging(String name, int level) { - if (name == null) { - throw new NullPointerException("name"); - } - if (level < Log.VERBOSE || level > Log.ASSERT) { - throw new IllegalArgumentException("Level is out of range [" - + Log.VERBOSE + ".." + Log.ASSERT + "]"); - } - - curlConfiguration = new LoggingConfiguration(name, level); - } - - /** - * Disables cURL logging for this client. - */ - public void disableCurlLogging() { - curlConfiguration = null; - } - - /** - * Logs cURL commands equivalent to requests. - */ - private class CurlLogger implements HttpRequestInterceptor { - @Override - public void process(HttpRequest request, HttpContext context) - throws HttpException, IOException { - LoggingConfiguration configuration = curlConfiguration; - if (configuration != null - && configuration.isLoggable() - && request instanceof HttpUriRequest) { - // Never print auth token -- we used to check ro.secure=0 to - // enable that, but can't do that in unbundled code. - configuration.println(toCurl((HttpUriRequest) request, false)); - } - } - } - - /** - * Generates a cURL command equivalent to the given request. - */ - private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException { - StringBuilder builder = new StringBuilder(); - - builder.append("curl "); - - // add in the method - builder.append("-X "); - builder.append(request.getMethod()); - builder.append(" "); - - for (Header header: request.getAllHeaders()) { - if (!logAuthToken - && (header.getName().equals("Authorization") || - header.getName().equals("Cookie"))) { - continue; - } - builder.append("--header \""); - builder.append(header.toString().trim()); - builder.append("\" "); - } - - URI uri = request.getURI(); - - // If this is a wrapped request, use the URI from the original - // request instead. getURI() on the wrapper seems to return a - // relative URI. We want an absolute URI. - if (request instanceof RequestWrapper) { - HttpRequest original = ((RequestWrapper) request).getOriginal(); - if (original instanceof HttpUriRequest) { - uri = ((HttpUriRequest) original).getURI(); - } - } - - builder.append("\""); - builder.append(uri); - builder.append("\""); - - if (request instanceof HttpEntityEnclosingRequest) { - HttpEntityEnclosingRequest entityRequest = - (HttpEntityEnclosingRequest) request; - HttpEntity entity = entityRequest.getEntity(); - if (entity != null && entity.isRepeatable()) { - if (entity.getContentLength() < 1024) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - entity.writeTo(stream); - - if (isBinaryContent(request)) { - String base64 = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP); - builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; "); - builder.append(" --data-binary @/tmp/$$.bin"); - } else { - String entityString = stream.toString(); - builder.append(" --data-ascii \"") - .append(entityString) - .append("\""); - } - } else { - builder.append(" [TOO MUCH DATA TO INCLUDE]"); - } - } - } - - return builder.toString(); - } - - public final static String CONTENT_ENCODING = "content-encoding"; - public final static String CONTENT_TYPE = "content-type"; - - private static boolean isBinaryContent(HttpUriRequest request) { - Header[] headers; - headers = request.getHeaders(CONTENT_ENCODING); - if (headers != null) { - for (Header header : headers) { - if ("gzip".equalsIgnoreCase(header.getValue())) { - return true; - } - } - } - - headers = request.getHeaders(CONTENT_TYPE); - if (headers != null) { - for (Header header : headers) { - for (String contentType : textContentTypes) { - if (header.getValue().startsWith(contentType)) { - return false; - } - } - } - } - return true; - } -} diff --git a/src/com/android/mms/service/http/NetworkAwareThreadSafeClientConnManager.java b/src/com/android/mms/service/http/NetworkAwareThreadSafeClientConnManager.java deleted file mode 100644 index a9175cd..0000000 --- a/src/com/android/mms/service/http/NetworkAwareThreadSafeClientConnManager.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2014 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 com.android.mms.service.http; - -import org.apache.http.conn.ClientConnectionOperator; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.HttpParams; - -/** - * This is a subclass of {@link org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager} - * which allows us to specify a custom name resolver and the address type - */ -public class NetworkAwareThreadSafeClientConnManager extends ThreadSafeClientConnManager { - public NetworkAwareThreadSafeClientConnManager(HttpParams params, - SchemeRegistry schreg, NameResolver resolver, boolean shouldUseIpv6) { - super(params, schreg); - ((NetworkAwareClientConnectionOperator)connOperator).setNameResolver(resolver); - ((NetworkAwareClientConnectionOperator)connOperator).setShouldUseIpv6(shouldUseIpv6); - } - - @Override - protected ClientConnectionOperator createConnectionOperator(SchemeRegistry schreg) { - return new NetworkAwareClientConnectionOperator(schreg); - } -} |