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 17package android.support.v7.mms; 18 19import android.content.Context; 20import android.os.Bundle; 21import android.telephony.SmsManager; 22import android.telephony.SubscriptionInfo; 23import android.telephony.SubscriptionManager; 24import android.telephony.TelephonyManager; 25import android.text.TextUtils; 26import android.util.Base64; 27import android.util.Log; 28 29import java.io.BufferedInputStream; 30import java.io.BufferedOutputStream; 31import java.io.ByteArrayOutputStream; 32import java.io.IOException; 33import java.io.InputStream; 34import java.io.OutputStream; 35import java.io.UnsupportedEncodingException; 36import java.lang.reflect.Method; 37import java.net.HttpURLConnection; 38import java.net.InetSocketAddress; 39import java.net.MalformedURLException; 40import java.net.ProtocolException; 41import java.net.Proxy; 42import java.net.URL; 43import java.util.List; 44import java.util.Locale; 45import java.util.Map; 46import java.util.regex.Matcher; 47import java.util.regex.Pattern; 48 49/** 50 * MMS HTTP client for sending and downloading MMS messages 51 */ 52public 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