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