diff options
Diffstat (limited to 'src/android/support/v7/mms/MmsHttpClient.java')
-rw-r--r-- | src/android/support/v7/mms/MmsHttpClient.java | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/src/android/support/v7/mms/MmsHttpClient.java b/src/android/support/v7/mms/MmsHttpClient.java new file mode 100644 index 0000000..8ca34da --- /dev/null +++ b/src/android/support/v7/mms/MmsHttpClient.java @@ -0,0 +1,523 @@ +/* + * 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.content.Context; +import android.os.Bundle; +import android.telephony.SmsManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +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.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +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 { + static final String METHOD_POST = "POST"; + 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_WITH_CHARSET = + "application/vnd.wap.mms-message; charset=utf-8"; + private static final String HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET = + "application/vnd.wap.mms-message"; + + /* + * Macro names + */ + // The raw phone number + private static final String MACRO_LINE1 = "LINE1"; + // The phone number without country code + private static final String MACRO_LINE1NOCOUNTRYCODE = "LINE1NOCOUNTRYCODE"; + // NAI (Network Access Identifier) + private static final String MACRO_NAI = "NAI"; + + // The possible NAI system property name + private static final String NAI_PROPERTY = "persist.radio.cdma.nai"; + + private final Context mContext; + private final TelephonyManager mTelephonyManager; + + /** + * Constructor + * + * @param context The Context object + */ + MmsHttpClient(Context context) { + mContext = context; + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + } + + /** + * 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 + * @param userAgent The user agent header value + * @param uaProfUrl The UA Prof URL header value + * @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, Bundle mmsConfig, String userAgent, String uaProfUrl) + throws MmsHttpException { + Log.d(MmsService.TAG, "HTTP: " + method + " " + Utils.redactUrlForNonVerbose(urlString) + + (isProxySet ? (", proxy=" + proxyHost + ":" + proxyPort) : "") + + ", PDU size=" + (pdu != null ? pdu.length : 0)); + checkMethod(method); + HttpURLConnection connection = null; + try { + Proxy proxy = Proxy.NO_PROXY; + if (isProxySet) { + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + } + final URL url = new URL(urlString); + // Now get the connection + connection = (HttpURLConnection) url.openConnection(proxy); + connection.setDoInput(true); + connection.setConnectTimeout( + mmsConfig.getInt(CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT, + CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT_DEFAULT)); + // ------- 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 + Log.i(MmsService.TAG, "HTTP: User-Agent=" + userAgent); + connection.setRequestProperty(HEADER_USER_AGENT, userAgent); + // Header: x-wap-profile + final String uaProfUrlTagName = mmsConfig.getString( + CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME, + CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME_DEFAULT); + if (uaProfUrl != null) { + Log.i(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); + if (mmsConfig.getBoolean( + CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER, + CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER_DEFAULT)) { + connection.setRequestProperty(HEADER_CONTENT_TYPE, + HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET); + } else { + connection.setRequestProperty(HEADER_CONTENT_TYPE, + HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET); + } + 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 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) { + throw new MmsHttpException(responseCode, responseMessage); + } + 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(); + Log.d(MmsService.TAG, "HTTP: response size=" + + (responseBody != null ? responseBody.length : 0)); + return responseBody; + } catch (MalformedURLException e) { + final String redactedUrl = Utils.redactUrlForNonVerbose(urlString); + Log.e(MmsService.TAG, "HTTP: invalid URL " + redactedUrl, e); + throw new MmsHttpException(0/*statusCode*/, "Invalid URL " + redactedUrl, e); + } catch (ProtocolException e) { + final String redactedUrl = Utils.redactUrlForNonVerbose(urlString); + Log.e(MmsService.TAG, "HTTP: invalid URL protocol " + redactedUrl, e); + throw new MmsHttpException(0/*statusCode*/, "Invalid URL protocol " + redactedUrl, e); + } catch (IOException e) { + Log.e(MmsService.TAG, "HTTP: IO failure", e); + throw new MmsHttpException(0/*statusCode*/, e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + 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 String resolveMacro(String value, Bundle 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 = getHttpParamMacro(macro, mmsConfig); + if (macroValue != null) { + replaced.append(macroValue); + } + 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, Bundle mmsConfig) { + final String extraHttpParams = mmsConfig.getString( + CarrierConfigValuesLoader.CONFIG_HTTP_PARAMS); + 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(splitPair[1].trim(), mmsConfig); + if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) { + // Add the header if the param is valid + connection.setRequestProperty(name, value); + } + } + } + } + } + + /** + * Return the HTTP param macro value. + * Example: LINE1 returns the phone number, etc. + * + * @param macro The macro name + * @param mmsConfig The carrier configuration values + * @return The value of the defined macro + */ + private String getHttpParamMacro(final String macro, final Bundle mmsConfig) { + if (MACRO_LINE1.equals(macro)) { + return getSelfNumber(); + } else if (MACRO_LINE1NOCOUNTRYCODE.equals(macro)) { + return PhoneNumberHelper.getNumberNoCountryCode( + getSelfNumber(), getSimOrLocaleCountry()); + } else if (MACRO_NAI.equals(macro)) { + return getEncodedNai(mmsConfig.getString( + CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX, + CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX_DEFAULT)); + } + return null; + } + + /** + * Get the device phone number + * + * @return the phone number text + */ + private String getSelfNumber() { + if (Utils.supportMSim()) { + final SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext); + final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo( + SmsManager.getDefaultSmsSubscriptionId()); + if (info != null) { + return info.getNumber(); + } else { + return null; + } + } else { + return mTelephonyManager.getLine1Number(); + } + } + + /** + * Get the country ISO code from SIM or system locale + * + * @return the country ISO code + */ + private String getSimOrLocaleCountry() { + String country = null; + if (Utils.supportMSim()) { + final SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext); + final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo( + SmsManager.getDefaultSmsSubscriptionId()); + if (info != null) { + country = info.getCountryIso(); + } + } else { + country = mTelephonyManager.getSimCountryIso(); + } + if (!TextUtils.isEmpty(country)) { + return country.toUpperCase(); + } else { + return Locale.getDefault().getCountry(); + } + } + + /** + * Get encoded NAI string to use as the HTTP header for some carriers. + * On L-MR1+, we call the hidden system API to get this + * On L-MR1-, we try to find it via system property. + * + * @param naiSuffix the suffix to append to NAI before encoding + * @return the Base64 encoded NAI string to use as HTTP header + */ + private String getEncodedNai(final String naiSuffix) { + String nai; + if (Utils.supportMSim()) { + nai = getNaiBySystemApi( + getSlotId(Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID))); + } else { + nai = getNaiBySystemProperty(); + } + if (!TextUtils.isEmpty(nai)) { + Log.i(MmsService.TAG, "NAI is not empty"); + if (!TextUtils.isEmpty(naiSuffix)) { + nai = nai + naiSuffix; + } + byte[] encoded = null; + try { + encoded = Base64.encode(nai.getBytes("UTF-8"), Base64.NO_WRAP); + } catch (UnsupportedEncodingException e) { + encoded = Base64.encode(nai.getBytes(), Base64.NO_WRAP); + } + try { + return new String(encoded, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return new String(encoded); + } + } + return null; + } + + /** + * Invoke hidden SubscriptionManager.getSlotId(int) + * + * @param subId the subId + * @return the SIM slot ID + */ + private static int getSlotId(final int subId) { + try { + final Method method = SubscriptionManager.class.getMethod("getSlotId", Integer.TYPE); + if (method != null) { + return (Integer) method.invoke(null, subId); + } + } catch (Exception e) { + Log.w(MmsService.TAG, "SubscriptionManager.getSlotId failed " + e); + } + return -1; + } + + /** + * Get NAI using hidden TelephonyManager.getNai(int) + * + * @param slotId the SIM slot ID + * @return the NAI string + */ + private String getNaiBySystemApi(final int slotId) { + try { + final Method method = mTelephonyManager.getClass().getMethod("getNai", Integer.TYPE); + if (method != null) { + return (String) method.invoke(mTelephonyManager, slotId); + } + } catch (Exception e) { + Log.w(MmsService.TAG, "TelephonyManager.getNai failed " + e); + } + return null; + } + + /** + * Get NAI using hidden SystemProperties.get(String) + * + * @return the NAI string as system property + */ + private static String getNaiBySystemProperty() { + try { + final Class systemPropertiesClass = Class.forName("android.os.SystemProperties"); + if (systemPropertiesClass != null) { + final Method method = systemPropertiesClass.getMethod("get", String.class); + if (method != null) { + return (String) method.invoke(null, NAI_PROPERTY); + } + } + } catch (Exception e) { + Log.w(MmsService.TAG, "SystemProperties.get failed " + e); + } + return null; + } +} |