• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.appcompat.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      */
MmsHttpClient(Context context)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      */
execute(String urlString, byte[] pdu, String method, boolean isProxySet, String proxyHost, int proxyPort, Bundle mmsConfig, String userAgent, String uaProfUrl)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 
logHttpHeaders(Map<String, List<String>> headers)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 
checkMethod(String method)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      */
getCurrentAcceptLanguage(Locale locale)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      */
convertObsoleteLanguageCodeToNew(String langCode)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 
addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale)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      */
resolveMacro(String value, Bundle mmsConfig)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      */
addExtraHeaders(HttpURLConnection connection, Bundle mmsConfig)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      */
getHttpParamMacro(final String macro, final Bundle mmsConfig)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      */
getSelfNumber()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      */
getSimOrLocaleCountry()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      */
getEncodedNai(final String naiSuffix)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.getSlotIndex(int)
470      *
471      * @param subId the subId
472      * @return the SIM slot ID
473      */
getSlotId(final int subId)474     private static int getSlotId(final int subId) {
475         try {
476             final Method method = SubscriptionManager.class.getMethod("getSlotIndex", Integer.TYPE);
477             if (method != null) {
478                 return (Integer) method.invoke(null, subId);
479             }
480         } catch (Exception e) {
481             Log.w(MmsService.TAG, "SubscriptionManager.getSlotIndex 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      */
getNaiBySystemApi(final int slotId)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      */
getNaiBySystemProperty()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