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 
17 package com.example.android.samplesync.client;
18 
19 import org.apache.http.HttpEntity;
20 import org.apache.http.HttpResponse;
21 import org.apache.http.HttpStatus;
22 import org.apache.http.NameValuePair;
23 import org.apache.http.ParseException;
24 import org.apache.http.auth.AuthenticationException;
25 import org.apache.http.client.HttpClient;
26 import org.apache.http.client.entity.UrlEncodedFormEntity;
27 import org.apache.http.client.methods.HttpPost;
28 import org.apache.http.conn.params.ConnManagerParams;
29 import org.apache.http.impl.client.DefaultHttpClient;
30 import org.apache.http.message.BasicNameValuePair;
31 import org.apache.http.params.HttpConnectionParams;
32 import org.apache.http.params.HttpParams;
33 import org.apache.http.util.EntityUtils;
34 import org.json.JSONArray;
35 import org.json.JSONException;
36 import org.json.JSONObject;
37 
38 import android.accounts.Account;
39 import android.graphics.Bitmap;
40 import android.graphics.BitmapFactory;
41 import android.text.TextUtils;
42 import android.util.Log;
43 
44 import java.io.BufferedReader;
45 import java.io.ByteArrayOutputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.InputStreamReader;
49 import java.io.UnsupportedEncodingException;
50 import java.net.HttpURLConnection;
51 import java.net.MalformedURLException;
52 import java.net.URL;
53 import java.util.ArrayList;
54 import java.util.List;
55 
56 /**
57  * Provides utility methods for communicating with the server.
58  */
59 final 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 
NetworkUtilities()81     private NetworkUtilities() {
82     }
83 
84     /**
85      * Configures the httpClient to connect to the URL provided.
86      */
getHttpClient()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      */
authenticate(String username, String password)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      */
syncContacts( Account account, String authtoken, long serverSyncState, List<RawContact> dirtyContacts)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      */
downloadAvatar(final String avatarUrl)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