1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.example.android.samplesync.client;
18
19import org.apache.http.HttpEntity;
20import org.apache.http.HttpResponse;
21import org.apache.http.HttpStatus;
22import org.apache.http.NameValuePair;
23import org.apache.http.ParseException;
24import org.apache.http.auth.AuthenticationException;
25import org.apache.http.client.HttpClient;
26import org.apache.http.client.entity.UrlEncodedFormEntity;
27import org.apache.http.client.methods.HttpPost;
28import org.apache.http.conn.params.ConnManagerParams;
29import org.apache.http.impl.client.DefaultHttpClient;
30import org.apache.http.message.BasicNameValuePair;
31import org.apache.http.params.HttpConnectionParams;
32import org.apache.http.params.HttpParams;
33import org.apache.http.util.EntityUtils;
34import org.json.JSONArray;
35import org.json.JSONException;
36import org.json.JSONObject;
37
38import android.accounts.Account;
39import android.graphics.Bitmap;
40import android.graphics.BitmapFactory;
41import android.text.TextUtils;
42import android.util.Log;
43
44import java.io.BufferedReader;
45import java.io.ByteArrayOutputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.io.InputStreamReader;
49import java.io.UnsupportedEncodingException;
50import java.net.HttpURLConnection;
51import java.net.MalformedURLException;
52import java.net.URL;
53import java.util.ArrayList;
54import java.util.List;
55
56/**
57 * Provides utility methods for communicating with the server.
58 */
59final public class NetworkUtilities {
60    /** The tag used to log to adb console. */
61    private static final String TAG = "NetworkUtilities";
62    /** POST parameter name for the user's account name */
63    public static final String PARAM_USERNAME = "username";
64    /** POST parameter name for the user's password */
65    public static final String PARAM_PASSWORD = "password";
66    /** POST parameter name for the user's authentication token */
67    public static final String PARAM_AUTH_TOKEN = "authtoken";
68    /** POST parameter name for the client's last-known sync state */
69    public static final String PARAM_SYNC_STATE = "syncstate";
70    /** POST parameter name for the sending client-edited contact info */
71    public static final String PARAM_CONTACTS_DATA = "contacts";
72    /** Timeout (in ms) we specify for each http request */
73    public static final int HTTP_REQUEST_TIMEOUT_MS = 30 * 1000;
74    /** Base URL for the v2 Sample Sync Service */
75    public static final String BASE_URL = "https://samplesyncadapter2.appspot.com";
76    /** URI for authentication service */
77    public static final String AUTH_URI = BASE_URL + "/auth";
78    /** URI for sync service */
79    public static final String SYNC_CONTACTS_URI = BASE_URL + "/sync";
80
81    private NetworkUtilities() {
82    }
83
84    /**
85     * Configures the httpClient to connect to the URL provided.
86     */
87    public static HttpClient getHttpClient() {
88        HttpClient httpClient = new DefaultHttpClient();
89        final HttpParams params = httpClient.getParams();
90        HttpConnectionParams.setConnectionTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
91        HttpConnectionParams.setSoTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
92        ConnManagerParams.setTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
93        return httpClient;
94    }
95
96    /**
97     * Connects to the SampleSync test server, authenticates the provided
98     * username and password.
99     *
100     * @param username The server account username
101     * @param password The server account password
102     * @return String The authentication token returned by the server (or null)
103     */
104    public static String authenticate(String username, String password) {
105
106        final HttpResponse resp;
107        final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
108        params.add(new BasicNameValuePair(PARAM_USERNAME, username));
109        params.add(new BasicNameValuePair(PARAM_PASSWORD, password));
110        final HttpEntity entity;
111        try {
112            entity = new UrlEncodedFormEntity(params);
113        } catch (final UnsupportedEncodingException e) {
114            // this should never happen.
115            throw new IllegalStateException(e);
116        }
117        Log.i(TAG, "Authenticating to: " + AUTH_URI);
118        final HttpPost post = new HttpPost(AUTH_URI);
119        post.addHeader(entity.getContentType());
120        post.setEntity(entity);
121        try {
122            resp = getHttpClient().execute(post);
123            String authToken = null;
124            if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
125                InputStream istream = (resp.getEntity() != null) ? resp.getEntity().getContent()
126                        : null;
127                if (istream != null) {
128                    BufferedReader ireader = new BufferedReader(new InputStreamReader(istream));
129                    authToken = ireader.readLine().trim();
130                }
131            }
132            if ((authToken != null) && (authToken.length() > 0)) {
133                Log.v(TAG, "Successful authentication");
134                return authToken;
135            } else {
136                Log.e(TAG, "Error authenticating" + resp.getStatusLine());
137                return null;
138            }
139        } catch (final IOException e) {
140            Log.e(TAG, "IOException when getting authtoken", e);
141            return null;
142        } finally {
143            Log.v(TAG, "getAuthtoken completing");
144        }
145    }
146
147    /**
148     * Perform 2-way sync with the server-side contacts. We send a request that
149     * includes all the locally-dirty contacts so that the server can process
150     * those changes, and we receive (and return) a list of contacts that were
151     * updated on the server-side that need to be updated locally.
152     *
153     * @param account The account being synced
154     * @param authtoken The authtoken stored in the AccountManager for this
155     *            account
156     * @param serverSyncState A token returned from the server on the last sync
157     * @param dirtyContacts A list of the contacts to send to the server
158     * @return A list of contacts that we need to update locally
159     */
160    public static List<RawContact> syncContacts(
161            Account account, String authtoken, long serverSyncState, List<RawContact> dirtyContacts)
162            throws JSONException, ParseException, IOException, AuthenticationException {
163        // Convert our list of User objects into a list of JSONObject
164        List<JSONObject> jsonContacts = new ArrayList<JSONObject>();
165        for (RawContact rawContact : dirtyContacts) {
166            jsonContacts.add(rawContact.toJSONObject());
167        }
168
169        // Create a special JSONArray of our JSON contacts
170        JSONArray buffer = new JSONArray(jsonContacts);
171
172        // Create an array that will hold the server-side contacts
173        // that have been changed (returned by the server).
174        final ArrayList<RawContact> serverDirtyList = new ArrayList<RawContact>();
175
176        // Prepare our POST data
177        final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
178        params.add(new BasicNameValuePair(PARAM_USERNAME, account.name));
179        params.add(new BasicNameValuePair(PARAM_AUTH_TOKEN, authtoken));
180        params.add(new BasicNameValuePair(PARAM_CONTACTS_DATA, buffer.toString()));
181        if (serverSyncState > 0) {
182            params.add(new BasicNameValuePair(PARAM_SYNC_STATE, Long.toString(serverSyncState)));
183        }
184        Log.i(TAG, params.toString());
185        HttpEntity entity = new UrlEncodedFormEntity(params);
186
187        // Send the updated friends data to the server
188        Log.i(TAG, "Syncing to: " + SYNC_CONTACTS_URI);
189        final HttpPost post = new HttpPost(SYNC_CONTACTS_URI);
190        post.addHeader(entity.getContentType());
191        post.setEntity(entity);
192        final HttpResponse resp = getHttpClient().execute(post);
193        final String response = EntityUtils.toString(resp.getEntity());
194        if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
195            // Our request to the server was successful - so we assume
196            // that they accepted all the changes we sent up, and
197            // that the response includes the contacts that we need
198            // to update on our side...
199            final JSONArray serverContacts = new JSONArray(response);
200            Log.d(TAG, response);
201            for (int i = 0; i < serverContacts.length(); i++) {
202                RawContact rawContact = RawContact.valueOf(serverContacts.getJSONObject(i));
203                if (rawContact != null) {
204                    serverDirtyList.add(rawContact);
205                }
206            }
207        } else {
208            if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
209                Log.e(TAG, "Authentication exception in sending dirty contacts");
210                throw new AuthenticationException();
211            } else {
212                Log.e(TAG, "Server error in sending dirty contacts: " + resp.getStatusLine());
213                throw new IOException();
214            }
215        }
216
217        return serverDirtyList;
218    }
219
220    /**
221     * Download the avatar image from the server.
222     *
223     * @param avatarUrl the URL pointing to the avatar image
224     * @return a byte array with the raw JPEG avatar image
225     */
226    public static byte[] downloadAvatar(final String avatarUrl) {
227        // If there is no avatar, we're done
228        if (TextUtils.isEmpty(avatarUrl)) {
229            return null;
230        }
231
232        try {
233            Log.i(TAG, "Downloading avatar: " + avatarUrl);
234            // Request the avatar image from the server, and create a bitmap
235            // object from the stream we get back.
236            URL url = new URL(avatarUrl);
237            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
238            connection.connect();
239            try {
240                final BitmapFactory.Options options = new BitmapFactory.Options();
241                final Bitmap avatar = BitmapFactory.decodeStream(connection.getInputStream(),
242                        null, options);
243
244                // Take the image we received from the server, whatever format it
245                // happens to be in, and convert it to a JPEG image. Note: we're
246                // not resizing the avatar - we assume that the image we get from
247                // the server is a reasonable size...
248                Log.i(TAG, "Converting avatar to JPEG");
249                ByteArrayOutputStream convertStream = new ByteArrayOutputStream(
250                        avatar.getWidth() * avatar.getHeight() * 4);
251                avatar.compress(Bitmap.CompressFormat.JPEG, 95, convertStream);
252                convertStream.flush();
253                convertStream.close();
254                // On pre-Honeycomb systems, it's important to call recycle on bitmaps
255                avatar.recycle();
256                return convertStream.toByteArray();
257            } finally {
258                connection.disconnect();
259            }
260        } catch (MalformedURLException muex) {
261            // A bad URL - nothing we can really do about it here...
262            Log.e(TAG, "Malformed avatar URL: " + avatarUrl);
263        } catch (IOException ioex) {
264            // If we're unable to download the avatar, it's a bummer but not the
265            // end of the world. We'll try to get it next time we sync.
266            Log.e(TAG, "Failed to download user avatar: " + avatarUrl);
267        }
268        return null;
269    }
270
271}
272