Home | History | Annotate | Download | only in mms
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.support.v7.mms;
     18 
     19 import android.content.Context;
     20 import android.os.Bundle;
     21 import android.telephony.SmsManager;
     22 import android.telephony.SubscriptionInfo;
     23 import android.telephony.SubscriptionManager;
     24 import android.telephony.TelephonyManager;
     25 import android.text.TextUtils;
     26 import android.util.Base64;
     27 import android.util.Log;
     28 
     29 import java.io.BufferedInputStream;
     30 import java.io.BufferedOutputStream;
     31 import java.io.ByteArrayOutputStream;
     32 import java.io.IOException;
     33 import java.io.InputStream;
     34 import java.io.OutputStream;
     35 import java.io.UnsupportedEncodingException;
     36 import java.lang.reflect.Method;
     37 import java.net.HttpURLConnection;
     38 import java.net.InetSocketAddress;
     39 import java.net.MalformedURLException;
     40 import java.net.ProtocolException;
     41 import java.net.Proxy;
     42 import java.net.URL;
     43 import java.util.List;
     44 import java.util.Locale;
     45 import java.util.Map;
     46 import java.util.regex.Matcher;
     47 import java.util.regex.Pattern;
     48 
     49 /**
     50  * MMS HTTP client for sending and downloading MMS messages
     51  */
     52 public class MmsHttpClient {
     53     static final String METHOD_POST = "POST";
     54     static final String METHOD_GET = "GET";
     55 
     56     private static final String HEADER_CONTENT_TYPE = "Content-Type";
     57     private static final String HEADER_ACCEPT = "Accept";
     58     private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language";
     59     private static final String HEADER_USER_AGENT = "User-Agent";
     60 
     61     // The "Accept" header value
     62     private static final String HEADER_VALUE_ACCEPT =
     63             "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic";
     64     // The "Content-Type" header value
     65     private static final String HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET =
     66             "application/vnd.wap.mms-message; charset=utf-8";
     67     private static final String HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET =
     68             "application/vnd.wap.mms-message";
     69 
     70     /*
     71      * Macro names
     72      */
     73     // The raw phone number
     74     private static final String MACRO_LINE1 = "LINE1";
     75     // The phone number without country code
     76     private static final String MACRO_LINE1NOCOUNTRYCODE = "LINE1NOCOUNTRYCODE";
     77     // NAI (Network Access Identifier)
     78     private static final String MACRO_NAI = "NAI";
     79 
     80     // The possible NAI system property name
     81     private static final String NAI_PROPERTY = "persist.radio.cdma.nai";
     82 
     83     private final Context mContext;
     84     private final TelephonyManager mTelephonyManager;
     85 
     86     /**
     87      * Constructor
     88      *
     89      * @param context The Context object
     90      */
     91     MmsHttpClient(Context context) {
     92         mContext = context;
     93         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
     94     }
     95 
     96     /**
     97      * Execute an MMS HTTP request, either a POST (sending) or a GET (downloading)
     98      *
     99      * @param urlString The request URL, for sending it is usually the MMSC, and for downloading
    100      *                  it is the message URL
    101      * @param pdu For POST (sending) only, the PDU to send
    102      * @param method HTTP method, POST for sending and GET for downloading
    103      * @param isProxySet Is there a proxy for the MMSC
    104      * @param proxyHost The proxy host
    105      * @param proxyPort The proxy port
    106      * @param mmsConfig The MMS config to use
    107      * @param userAgent The user agent header value
    108      * @param uaProfUrl The UA Prof URL header value
    109      * @return The HTTP response body
    110      * @throws MmsHttpException For any failures
    111      */
    112     public byte[] execute(String urlString, byte[] pdu, String method, boolean isProxySet,
    113             String proxyHost, int proxyPort, Bundle mmsConfig, String userAgent, String uaProfUrl)
    114             throws MmsHttpException {
    115         Log.d(MmsService.TAG, "HTTP: " + method + " " + Utils.redactUrlForNonVerbose(urlString)
    116                 + (isProxySet ? (", proxy=" + proxyHost + ":" + proxyPort) : "")
    117                 + ", PDU size=" + (pdu != null ? pdu.length : 0));
    118         checkMethod(method);
    119         HttpURLConnection connection = null;
    120         try {
    121             Proxy proxy = Proxy.NO_PROXY;
    122             if (isProxySet) {
    123                 proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
    124             }
    125             final URL url = new URL(urlString);
    126             // Now get the connection
    127             connection = (HttpURLConnection) url.openConnection(proxy);
    128             connection.setDoInput(true);
    129             connection.setConnectTimeout(
    130                     mmsConfig.getInt(CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT,
    131                             CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT_DEFAULT));
    132             // ------- COMMON HEADERS ---------
    133             // Header: Accept
    134             connection.setRequestProperty(HEADER_ACCEPT, HEADER_VALUE_ACCEPT);
    135             // Header: Accept-Language
    136             connection.setRequestProperty(
    137                     HEADER_ACCEPT_LANGUAGE, getCurrentAcceptLanguage(Locale.getDefault()));
    138             // Header: User-Agent
    139             Log.i(MmsService.TAG, "HTTP: User-Agent=" + userAgent);
    140             connection.setRequestProperty(HEADER_USER_AGENT, userAgent);
    141             // Header: x-wap-profile
    142             final String uaProfUrlTagName = mmsConfig.getString(
    143                     CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME,
    144                     CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME_DEFAULT);
    145             if (uaProfUrl != null) {
    146                 Log.i(MmsService.TAG, "HTTP: UaProfUrl=" + uaProfUrl);
    147                 connection.setRequestProperty(uaProfUrlTagName, uaProfUrl);
    148             }
    149             // Add extra headers specified by mms_config.xml's httpparams
    150             addExtraHeaders(connection, mmsConfig);
    151             // Different stuff for GET and POST
    152             if (METHOD_POST.equals(method)) {
    153                 if (pdu == null || pdu.length < 1) {
    154                     Log.e(MmsService.TAG, "HTTP: empty pdu");
    155                     throw new MmsHttpException(0/*statusCode*/, "Sending empty PDU");
    156                 }
    157                 connection.setDoOutput(true);
    158                 connection.setRequestMethod(METHOD_POST);
    159                 if (mmsConfig.getBoolean(
    160                         CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER,
    161                         CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER_DEFAULT)) {
    162                     connection.setRequestProperty(HEADER_CONTENT_TYPE,
    163                             HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET);
    164                 } else {
    165                     connection.setRequestProperty(HEADER_CONTENT_TYPE,
    166                             HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET);
    167                 }
    168                 if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) {
    169                     logHttpHeaders(connection.getRequestProperties());
    170                 }
    171                 connection.setFixedLengthStreamingMode(pdu.length);
    172                 // Sending request body
    173                 final OutputStream out =
    174                         new BufferedOutputStream(connection.getOutputStream());
    175                 out.write(pdu);
    176                 out.flush();
    177                 out.close();
    178             } else if (METHOD_GET.equals(method)) {
    179                 if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) {
    180                     logHttpHeaders(connection.getRequestProperties());
    181                 }
    182                 connection.setRequestMethod(METHOD_GET);
    183             }
    184             // Get response
    185             final int responseCode = connection.getResponseCode();
    186             final String responseMessage = connection.getResponseMessage();
    187             Log.d(MmsService.TAG, "HTTP: " + responseCode + " " + responseMessage);
    188             if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) {
    189                 logHttpHeaders(connection.getHeaderFields());
    190             }
    191             if (responseCode / 100 != 2) {
    192                 throw new MmsHttpException(responseCode, responseMessage);
    193             }
    194             final InputStream in = new BufferedInputStream(connection.getInputStream());
    195             final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
    196             final byte[] buf = new byte[4096];
    197             int count = 0;
    198             while ((count = in.read(buf)) > 0) {
    199                 byteOut.write(buf, 0, count);
    200             }
    201             in.close();
    202             final byte[] responseBody = byteOut.toByteArray();
    203             Log.d(MmsService.TAG, "HTTP: response size="
    204                     + (responseBody != null ? responseBody.length : 0));
    205             return responseBody;
    206         } catch (MalformedURLException e) {
    207             final String redactedUrl = Utils.redactUrlForNonVerbose(urlString);
    208             Log.e(MmsService.TAG, "HTTP: invalid URL " + redactedUrl, e);
    209             throw new MmsHttpException(0/*statusCode*/, "Invalid URL " + redactedUrl, e);
    210         } catch (ProtocolException e) {
    211             final String redactedUrl = Utils.redactUrlForNonVerbose(urlString);
    212             Log.e(MmsService.TAG, "HTTP: invalid URL protocol " + redactedUrl, e);
    213             throw new MmsHttpException(0/*statusCode*/, "Invalid URL protocol " + redactedUrl, e);
    214         } catch (IOException e) {
    215             Log.e(MmsService.TAG, "HTTP: IO failure", e);
    216             throw new MmsHttpException(0/*statusCode*/, e);
    217         } finally {
    218             if (connection != null) {
    219                 connection.disconnect();
    220             }
    221         }
    222     }
    223 
    224     private static void logHttpHeaders(Map<String, List<String>> headers) {
    225         final StringBuilder sb = new StringBuilder();
    226         if (headers != null) {
    227             for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
    228                 final String key = entry.getKey();
    229                 final List<String> values = entry.getValue();
    230                 if (values != null) {
    231                     for (String value : values) {
    232                         sb.append(key).append('=').append(value).append('\n');
    233                     }
    234                 }
    235             }
    236             Log.v(MmsService.TAG, "HTTP: headers\n" + sb.toString());
    237         }
    238     }
    239 
    240     private static void checkMethod(String method) throws MmsHttpException {
    241         if (!METHOD_GET.equals(method) && !METHOD_POST.equals(method)) {
    242             throw new MmsHttpException(0/*statusCode*/, "Invalid method " + method);
    243         }
    244     }
    245 
    246     private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US";
    247 
    248     /**
    249      * Return the Accept-Language header.  Use the current locale plus
    250      * US if we are in a different locale than US.
    251      * This code copied from the browser's WebSettings.java
    252      *
    253      * @return Current AcceptLanguage String.
    254      */
    255     public static String getCurrentAcceptLanguage(Locale locale) {
    256         final StringBuilder buffer = new StringBuilder();
    257         addLocaleToHttpAcceptLanguage(buffer, locale);
    258 
    259         if (!Locale.US.equals(locale)) {
    260             if (buffer.length() > 0) {
    261                 buffer.append(", ");
    262             }
    263             buffer.append(ACCEPT_LANG_FOR_US_LOCALE);
    264         }
    265 
    266         return buffer.toString();
    267     }
    268 
    269     /**
    270      * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish,
    271      * to new standard.
    272      */
    273     private static String convertObsoleteLanguageCodeToNew(String langCode) {
    274         if (langCode == null) {
    275             return null;
    276         }
    277         if ("iw".equals(langCode)) {
    278             // Hebrew
    279             return "he";
    280         } else if ("in".equals(langCode)) {
    281             // Indonesian
    282             return "id";
    283         } else if ("ji".equals(langCode)) {
    284             // Yiddish
    285             return "yi";
    286         }
    287         return langCode;
    288     }
    289 
    290     private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale) {
    291         final String language = convertObsoleteLanguageCodeToNew(locale.getLanguage());
    292         if (language != null) {
    293             builder.append(language);
    294             final String country = locale.getCountry();
    295             if (country != null) {
    296                 builder.append("-");
    297                 builder.append(country);
    298             }
    299         }
    300     }
    301 
    302     private static final Pattern MACRO_P = Pattern.compile("##(\\S+)##");
    303     /**
    304      * Resolve the macro in HTTP param value text
    305      * For example, "something##LINE1##something" is resolved to "something9139531419something"
    306      *
    307      * @param value The HTTP param value possibly containing macros
    308      * @return The HTTP param with macro resolved to real value
    309      */
    310     private String resolveMacro(String value, Bundle mmsConfig) {
    311         if (TextUtils.isEmpty(value)) {
    312             return value;
    313         }
    314         final Matcher matcher = MACRO_P.matcher(value);
    315         int nextStart = 0;
    316         StringBuilder replaced = null;
    317         while (matcher.find()) {
    318             if (replaced == null) {
    319                 replaced = new StringBuilder();
    320             }
    321             final int matchedStart = matcher.start();
    322             if (matchedStart > nextStart) {
    323                 replaced.append(value.substring(nextStart, matchedStart));
    324             }
    325             final String macro = matcher.group(1);
    326             final String macroValue = getHttpParamMacro(macro, mmsConfig);
    327             if (macroValue != null) {
    328                 replaced.append(macroValue);
    329             }
    330             nextStart = matcher.end();
    331         }
    332         if (replaced != null && nextStart < value.length()) {
    333             replaced.append(value.substring(nextStart));
    334         }
    335         return replaced == null ? value : replaced.toString();
    336     }
    337 
    338     /**
    339      * Add extra HTTP headers from mms_config.xml's httpParams, which is a list of key/value
    340      * pairs separated by "|". Each key/value pair is separated by ":". Value may contain
    341      * macros like "##LINE1##" or "##NAI##" which is resolved with methods in this class
    342      *
    343      * @param connection The HttpURLConnection that we add headers to
    344      * @param mmsConfig The MmsConfig object
    345      */
    346     private void addExtraHeaders(HttpURLConnection connection, Bundle mmsConfig) {
    347         final String extraHttpParams = mmsConfig.getString(
    348                 CarrierConfigValuesLoader.CONFIG_HTTP_PARAMS);
    349         if (!TextUtils.isEmpty(extraHttpParams)) {
    350             // Parse the parameter list
    351             String paramList[] = extraHttpParams.split("\\|");
    352             for (String paramPair : paramList) {
    353                 String splitPair[] = paramPair.split(":", 2);
    354                 if (splitPair.length == 2) {
    355                     final String name = splitPair[0].trim();
    356                     final String value = resolveMacro(splitPair[1].trim(), mmsConfig);
    357                     if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) {
    358                         // Add the header if the param is valid
    359                         connection.setRequestProperty(name, value);
    360                     }
    361                 }
    362             }
    363         }
    364     }
    365 
    366     /**
    367      * Return the HTTP param macro value.
    368      * Example: LINE1 returns the phone number, etc.
    369      *
    370      * @param macro The macro name
    371      * @param mmsConfig The carrier configuration values
    372      * @return The value of the defined macro
    373      */
    374     private String getHttpParamMacro(final String macro, final Bundle mmsConfig) {
    375         if (MACRO_LINE1.equals(macro)) {
    376             return getSelfNumber();
    377         } else if (MACRO_LINE1NOCOUNTRYCODE.equals(macro)) {
    378             return PhoneNumberHelper.getNumberNoCountryCode(
    379                     getSelfNumber(), getSimOrLocaleCountry());
    380         }  else if (MACRO_NAI.equals(macro)) {
    381             return getEncodedNai(mmsConfig.getString(
    382                     CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX,
    383                     CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX_DEFAULT));
    384         }
    385         return null;
    386     }
    387 
    388     /**
    389      * Get the device phone number
    390      *
    391      * @return the phone number text
    392      */
    393     private String getSelfNumber() {
    394         if (Utils.supportMSim()) {
    395             final SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext);
    396             final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo(
    397                     SmsManager.getDefaultSmsSubscriptionId());
    398             if (info != null) {
    399                 return info.getNumber();
    400             } else {
    401                 return null;
    402             }
    403         } else {
    404             return mTelephonyManager.getLine1Number();
    405         }
    406     }
    407 
    408     /**
    409      * Get the country ISO code from SIM or system locale
    410      *
    411      * @return the country ISO code
    412      */
    413     private String getSimOrLocaleCountry() {
    414         String country = null;
    415         if (Utils.supportMSim()) {
    416             final SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext);
    417             final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo(
    418                     SmsManager.getDefaultSmsSubscriptionId());
    419             if (info != null) {
    420                 country = info.getCountryIso();
    421             }
    422         } else {
    423             country = mTelephonyManager.getSimCountryIso();
    424         }
    425         if (!TextUtils.isEmpty(country)) {
    426             return country.toUpperCase();
    427         } else {
    428             return Locale.getDefault().getCountry();
    429         }
    430     }
    431 
    432     /**
    433      * Get encoded NAI string to use as the HTTP header for some carriers.
    434      * On L-MR1+, we call the hidden system API to get this
    435      * On L-MR1-, we try to find it via system property.
    436      *
    437      * @param naiSuffix the suffix to append to NAI before encoding
    438      * @return the Base64 encoded NAI string to use as HTTP header
    439      */
    440     private String getEncodedNai(final String naiSuffix) {
    441         String nai;
    442         if (Utils.supportMSim()) {
    443             nai = getNaiBySystemApi(
    444                     getSlotId(Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID)));
    445         } else {
    446             nai = getNaiBySystemProperty();
    447         }
    448         if (!TextUtils.isEmpty(nai)) {
    449             Log.i(MmsService.TAG, "NAI is not empty");
    450             if (!TextUtils.isEmpty(naiSuffix)) {
    451                 nai = nai + naiSuffix;
    452             }
    453             byte[] encoded = null;
    454             try {
    455                 encoded = Base64.encode(nai.getBytes("UTF-8"), Base64.NO_WRAP);
    456             } catch (UnsupportedEncodingException e) {
    457                 encoded = Base64.encode(nai.getBytes(), Base64.NO_WRAP);
    458             }
    459             try {
    460                 return new String(encoded, "UTF-8");
    461             } catch (UnsupportedEncodingException e) {
    462                 return new String(encoded);
    463             }
    464         }
    465         return null;
    466     }
    467 
    468     /**
    469      * Invoke hidden SubscriptionManager.getSlotId(int)
    470      *
    471      * @param subId the subId
    472      * @return the SIM slot ID
    473      */
    474     private static int getSlotId(final int subId) {
    475         try {
    476             final Method method = SubscriptionManager.class.getMethod("getSlotId", Integer.TYPE);
    477             if (method != null) {
    478                 return (Integer) method.invoke(null, subId);
    479             }
    480         } catch (Exception e) {
    481             Log.w(MmsService.TAG, "SubscriptionManager.getSlotId failed " + e);
    482         }
    483         return -1;
    484     }
    485 
    486     /**
    487      * Get NAI using hidden TelephonyManager.getNai(int)
    488      *
    489      * @param slotId the SIM slot ID
    490      * @return the NAI string
    491      */
    492     private String getNaiBySystemApi(final int slotId) {
    493         try {
    494             final Method method = mTelephonyManager.getClass().getMethod("getNai", Integer.TYPE);
    495             if (method != null) {
    496                 return (String) method.invoke(mTelephonyManager, slotId);
    497             }
    498         } catch (Exception e) {
    499             Log.w(MmsService.TAG, "TelephonyManager.getNai failed " + e);
    500         }
    501         return null;
    502     }
    503 
    504     /**
    505      * Get NAI using hidden SystemProperties.get(String)
    506      *
    507      * @return the NAI string as system property
    508      */
    509     private static String getNaiBySystemProperty() {
    510         try {
    511             final Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
    512             if (systemPropertiesClass != null) {
    513                 final Method method = systemPropertiesClass.getMethod("get", String.class);
    514                 if (method != null) {
    515                     return (String) method.invoke(null, NAI_PROPERTY);
    516                 }
    517             }
    518         } catch (Exception e) {
    519             Log.w(MmsService.TAG, "SystemProperties.get failed " + e);
    520         }
    521         return null;
    522     }
    523 }
    524