Home | History | Annotate | Download | only in transaction
      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 
     18 package com.android.mms.transaction;
     19 
     20 import org.apache.http.HttpEntity;
     21 import org.apache.http.HttpHost;
     22 import org.apache.http.HttpRequest;
     23 import org.apache.http.HttpResponse;
     24 import org.apache.http.StatusLine;
     25 import org.apache.http.client.methods.HttpGet;
     26 import org.apache.http.client.methods.HttpPost;
     27 import org.apache.http.conn.params.ConnRouteParams;
     28 import org.apache.http.params.HttpParams;
     29 import org.apache.http.params.HttpProtocolParams;
     30 import org.apache.http.params.HttpConnectionParams;
     31 import org.apache.http.Header;
     32 
     33 import com.android.mms.MmsConfig;
     34 import com.android.mms.LogTag;
     35 
     36 import android.content.Context;
     37 import android.net.http.AndroidHttpClient;
     38 import android.telephony.TelephonyManager;
     39 import android.text.TextUtils;
     40 import android.util.Config;
     41 import android.util.Log;
     42 
     43 import java.io.ByteArrayOutputStream;
     44 import java.io.DataInputStream;
     45 import java.io.InputStream;
     46 import java.io.IOException;
     47 import java.net.SocketException;
     48 import java.net.URI;
     49 import java.net.URISyntaxException;
     50 import java.util.Locale;
     51 
     52 public 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