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