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