Home | History | Annotate | Download | only in client
      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