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 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