1/* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.mms.transaction; 19 20import org.apache.http.HttpEntity; 21import org.apache.http.HttpHost; 22import org.apache.http.HttpRequest; 23import org.apache.http.HttpResponse; 24import org.apache.http.StatusLine; 25import org.apache.http.client.methods.HttpGet; 26import org.apache.http.client.methods.HttpPost; 27import org.apache.http.conn.params.ConnRouteParams; 28import org.apache.http.params.HttpParams; 29import org.apache.http.params.HttpProtocolParams; 30import org.apache.http.params.HttpConnectionParams; 31import org.apache.http.Header; 32 33import com.android.mms.MmsConfig; 34import com.android.mms.LogTag; 35 36import android.content.Context; 37import android.net.http.AndroidHttpClient; 38import android.telephony.TelephonyManager; 39import android.text.TextUtils; 40import android.util.Config; 41import android.util.Log; 42 43import java.io.ByteArrayOutputStream; 44import java.io.DataInputStream; 45import java.io.InputStream; 46import java.io.IOException; 47import java.net.SocketException; 48import java.net.URI; 49import java.net.URISyntaxException; 50import java.util.Locale; 51 52public class HttpUtils { 53 private static final String TAG = LogTag.TRANSACTION; 54 55 private static final boolean DEBUG = false; 56 private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; 57 58 public static final int HTTP_POST_METHOD = 1; 59 public static final int HTTP_GET_METHOD = 2; 60 61 private static final int MMS_READ_BUFFER = 4096; 62 63 // This is the value to use for the "Accept-Language" header. 64 // Once it becomes possible for the user to change the locale 65 // setting, this should no longer be static. We should call 66 // getHttpAcceptLanguage instead. 67 private static final String HDR_VALUE_ACCEPT_LANGUAGE; 68 69 static { 70 HDR_VALUE_ACCEPT_LANGUAGE = getCurrentAcceptLanguage(Locale.getDefault()); 71 } 72 73 // Definition for necessary HTTP headers. 74 private static final String HDR_KEY_ACCEPT = "Accept"; 75 private static final String HDR_KEY_ACCEPT_LANGUAGE = "Accept-Language"; 76 77 private static final String HDR_VALUE_ACCEPT = 78 "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"; 79 80 private HttpUtils() { 81 // To forbidden instantiate this class. 82 } 83 84 /** 85 * A helper method to send or retrieve data through HTTP protocol. 86 * 87 * @param token The token to identify the sending progress. 88 * @param url The URL used in a GET request. Null when the method is 89 * HTTP_POST_METHOD. 90 * @param pdu The data to be POST. Null when the method is HTTP_GET_METHOD. 91 * @param method HTTP_POST_METHOD or HTTP_GET_METHOD. 92 * @return A byte array which contains the response data. 93 * If an HTTP error code is returned, an IOException will be thrown. 94 * @throws IOException if any error occurred on network interface or 95 * an HTTP error code(>=400) returned from the server. 96 */ 97 protected static byte[] httpConnection(Context context, long token, 98 String url, byte[] pdu, int method, boolean isProxySet, 99 String proxyHost, int proxyPort) throws IOException { 100 if (url == null) { 101 throw new IllegalArgumentException("URL must not be null."); 102 } 103 104 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 105 Log.v(TAG, "httpConnection: params list"); 106 Log.v(TAG, "\ttoken\t\t= " + token); 107 Log.v(TAG, "\turl\t\t= " + url); 108 Log.v(TAG, "\tmethod\t\t= " 109 + ((method == HTTP_POST_METHOD) ? "POST" 110 : ((method == HTTP_GET_METHOD) ? "GET" : "UNKNOWN"))); 111 Log.v(TAG, "\tisProxySet\t= " + isProxySet); 112 Log.v(TAG, "\tproxyHost\t= " + proxyHost); 113 Log.v(TAG, "\tproxyPort\t= " + proxyPort); 114 // TODO Print out binary data more readable. 115 //Log.v(TAG, "\tpdu\t\t= " + Arrays.toString(pdu)); 116 } 117 118 AndroidHttpClient client = null; 119 120 try { 121 // Make sure to use a proxy which supports CONNECT. 122 URI hostUrl = new URI(url); 123 HttpHost target = new HttpHost( 124 hostUrl.getHost(), hostUrl.getPort(), 125 HttpHost.DEFAULT_SCHEME_NAME); 126 127 client = createHttpClient(context); 128 HttpRequest req = null; 129 switch(method) { 130 case HTTP_POST_METHOD: 131 ProgressCallbackEntity entity = new ProgressCallbackEntity( 132 context, token, pdu); 133 // Set request content type. 134 entity.setContentType("application/vnd.wap.mms-message"); 135 136 HttpPost post = new HttpPost(url); 137 post.setEntity(entity); 138 req = post; 139 break; 140 case HTTP_GET_METHOD: 141 req = new HttpGet(url); 142 break; 143 default: 144 Log.e(TAG, "Unknown HTTP method: " + method 145 + ". Must be one of POST[" + HTTP_POST_METHOD 146 + "] or GET[" + HTTP_GET_METHOD + "]."); 147 return null; 148 } 149 150 // Set route parameters for the request. 151 HttpParams params = client.getParams(); 152 if (isProxySet) { 153 ConnRouteParams.setDefaultProxy( 154 params, new HttpHost(proxyHost, proxyPort)); 155 } 156 req.setParams(params); 157 158 // Set necessary HTTP headers for MMS transmission. 159 req.addHeader(HDR_KEY_ACCEPT, HDR_VALUE_ACCEPT); 160 { 161 String xWapProfileTagName = MmsConfig.getUaProfTagName(); 162 String xWapProfileUrl = MmsConfig.getUaProfUrl(); 163 164 if (xWapProfileUrl != null) { 165 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 166 Log.d(LogTag.TRANSACTION, 167 "[HttpUtils] httpConn: xWapProfUrl=" + xWapProfileUrl); 168 } 169 req.addHeader(xWapProfileTagName, xWapProfileUrl); 170 } 171 } 172 173 // Extra http parameters. Split by '|' to get a list of value pairs. 174 // Separate each pair by the first occurrence of ':' to obtain a name and 175 // value. Replace the occurrence of the string returned by 176 // MmsConfig.getHttpParamsLine1Key() with the users telephone number inside 177 // the value. 178 String extraHttpParams = MmsConfig.getHttpParams(); 179 180 if (extraHttpParams != null) { 181 String line1Number = ((TelephonyManager)context 182 .getSystemService(Context.TELEPHONY_SERVICE)) 183 .getLine1Number(); 184 String line1Key = MmsConfig.getHttpParamsLine1Key(); 185 String paramList[] = extraHttpParams.split("\\|"); 186 187 for (String paramPair : paramList) { 188 String splitPair[] = paramPair.split(":", 2); 189 190 if (splitPair.length == 2) { 191 String name = splitPair[0].trim(); 192 String value = splitPair[1].trim(); 193 194 if (line1Key != null) { 195 value = value.replace(line1Key, line1Number); 196 } 197 if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) { 198 req.addHeader(name, value); 199 } 200 } 201 } 202 } 203 req.addHeader(HDR_KEY_ACCEPT_LANGUAGE, HDR_VALUE_ACCEPT_LANGUAGE); 204 205 HttpResponse response = client.execute(target, req); 206 StatusLine status = response.getStatusLine(); 207 if (status.getStatusCode() != 200) { // HTTP 200 is success. 208 throw new IOException("HTTP error: " + status.getReasonPhrase()); 209 } 210 211 HttpEntity entity = response.getEntity(); 212 byte[] body = null; 213 if (entity != null) { 214 try { 215 if (entity.getContentLength() > 0) { 216 body = new byte[(int) entity.getContentLength()]; 217 DataInputStream dis = new DataInputStream(entity.getContent()); 218 try { 219 dis.readFully(body); 220 } finally { 221 try { 222 dis.close(); 223 } catch (IOException e) { 224 Log.e(TAG, "Error closing input stream: " + e.getMessage()); 225 } 226 } 227 } 228 if (entity.isChunked()) { 229 Log.v(TAG, "httpConnection: transfer encoding is chunked"); 230 int bytesTobeRead = MmsConfig.getMaxMessageSize(); 231 byte[] tempBody = new byte[bytesTobeRead]; 232 DataInputStream dis = new DataInputStream(entity.getContent()); 233 try { 234 int bytesRead = 0; 235 int offset = 0; 236 boolean readError = false; 237 do { 238 try { 239 bytesRead = dis.read(tempBody, offset, bytesTobeRead); 240 } catch (IOException e) { 241 readError = true; 242 Log.e(TAG, "httpConnection: error reading input stream" 243 + e.getMessage()); 244 break; 245 } 246 if (bytesRead > 0) { 247 bytesTobeRead -= bytesRead; 248 offset += bytesRead; 249 } 250 } while (bytesRead >= 0 && bytesTobeRead > 0); 251 if (bytesRead == -1 && offset > 0 && !readError) { 252 // offset is same as total number of bytes read 253 // bytesRead will be -1 if the data was read till the eof 254 body = new byte[offset]; 255 System.arraycopy(tempBody, 0, body, 0, offset); 256 Log.v(TAG, "httpConnection: Chunked response length [" 257 + Integer.toString(offset) + "]"); 258 } else { 259 Log.e(TAG, "httpConnection: Response entity too large or empty"); 260 } 261 } finally { 262 try { 263 dis.close(); 264 } catch (IOException e) { 265 Log.e(TAG, "Error closing input stream: " + e.getMessage()); 266 } 267 } 268 } 269 } finally { 270 if (entity != null) { 271 entity.consumeContent(); 272 } 273 } 274 } 275 return body; 276 } catch (URISyntaxException e) { 277 handleHttpConnectionException(e, url); 278 } catch (IllegalStateException e) { 279 handleHttpConnectionException(e, url); 280 } catch (IllegalArgumentException e) { 281 handleHttpConnectionException(e, url); 282 } catch (SocketException e) { 283 handleHttpConnectionException(e, url); 284 } catch (Exception e) { 285 handleHttpConnectionException(e, url); 286 } 287 finally { 288 if (client != null) { 289 client.close(); 290 } 291 } 292 return null; 293 } 294 295 private static void handleHttpConnectionException(Exception exception, String url) 296 throws IOException { 297 // Inner exception should be logged to make life easier. 298 Log.e(TAG, "Url: " + url + "\n" + exception.getMessage()); 299 IOException e = new IOException(exception.getMessage()); 300 e.initCause(exception); 301 throw e; 302 } 303 304 private static AndroidHttpClient createHttpClient(Context context) { 305 String userAgent = MmsConfig.getUserAgent(); 306 AndroidHttpClient client = AndroidHttpClient.newInstance(userAgent, context); 307 HttpParams params = client.getParams(); 308 HttpProtocolParams.setContentCharset(params, "UTF-8"); 309 310 // set the socket timeout 311 int soTimeout = MmsConfig.getHttpSocketTimeout(); 312 313 if (Log.isLoggable(LogTag.TRANSACTION, Log.DEBUG)) { 314 Log.d(TAG, "[HttpUtils] createHttpClient w/ socket timeout " + soTimeout + " ms, " 315 + ", UA=" + userAgent); 316 } 317 HttpConnectionParams.setSoTimeout(params, soTimeout); 318 return client; 319 } 320 321 private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US"; 322 323 /** 324 * Return the Accept-Language header. Use the current locale plus 325 * US if we are in a different locale than US. 326 * This code copied from the browser's WebSettings.java 327 * @return Current AcceptLanguage String. 328 */ 329 public static String getCurrentAcceptLanguage(Locale locale) { 330 StringBuilder buffer = new StringBuilder(); 331 addLocaleToHttpAcceptLanguage(buffer, locale); 332 333 if (!Locale.US.equals(locale)) { 334 if (buffer.length() > 0) { 335 buffer.append(", "); 336 } 337 buffer.append(ACCEPT_LANG_FOR_US_LOCALE); 338 } 339 340 return buffer.toString(); 341 } 342 343 /** 344 * Convert obsolete language codes, including Hebrew/Indonesian/Yiddish, 345 * to new standard. 346 */ 347 private static String convertObsoleteLanguageCodeToNew(String langCode) { 348 if (langCode == null) { 349 return null; 350 } 351 if ("iw".equals(langCode)) { 352 // Hebrew 353 return "he"; 354 } else if ("in".equals(langCode)) { 355 // Indonesian 356 return "id"; 357 } else if ("ji".equals(langCode)) { 358 // Yiddish 359 return "yi"; 360 } 361 return langCode; 362 } 363 364 private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, 365 Locale locale) { 366 String language = convertObsoleteLanguageCodeToNew(locale.getLanguage()); 367 if (language != null) { 368 builder.append(language); 369 String country = locale.getCountry(); 370 if (country != null) { 371 builder.append("-"); 372 builder.append(country); 373 } 374 } 375 } 376} 377