Home | History | Annotate | Download | only in picasa
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of 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,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.cooliris.picasa;
     18 
     19 import java.io.IOException;
     20 import java.net.SocketException;
     21 import java.util.ArrayList;
     22 
     23 import org.apache.http.HttpStatus;
     24 import org.xml.sax.SAXException;
     25 
     26 import android.accounts.Account;
     27 import android.accounts.AccountManager;
     28 import android.accounts.AuthenticatorException;
     29 import android.accounts.OperationCanceledException;
     30 import android.app.Activity;
     31 import android.content.Context;
     32 import android.content.SyncResult;
     33 import android.net.Uri;
     34 import android.os.Bundle;
     35 import android.util.Log;
     36 import android.util.Xml;
     37 
     38 public final class PicasaApi {
     39     public static final int RESULT_OK = 0;
     40     public static final int RESULT_NOT_MODIFIED = 1;
     41     public static final int RESULT_ERROR = 2;
     42 
     43     private static final String TAG = "PicasaAPI";
     44     private static final String BASE_URL = "http://picasaweb.google.com/data/feed/api/";
     45     private static final String BASE_QUERY_STRING;
     46 
     47     static {
     48         // Build the base query string using screen dimensions.
     49         final StringBuilder query = new StringBuilder("?imgmax=1024&max-results=1000&thumbsize=");
     50         final String thumbnailSize = "144u,";
     51         final String screennailSize = "1024u";
     52         query.append(thumbnailSize);
     53         query.append(screennailSize);
     54         BASE_QUERY_STRING = query.toString() + "&visibility=visible";
     55     }
     56 
     57     private final GDataClient mClient;
     58     private final GDataClient.Operation mOperation = new GDataClient.Operation();
     59     private final GDataParser mParser = new GDataParser();
     60     private final AlbumEntry mAlbumInstance = new AlbumEntry();
     61     private final PhotoEntry mPhotoInstance = new PhotoEntry();
     62     private AuthAccount mAuth;
     63 
     64     public static final class AuthAccount {
     65         public final String user;
     66         public final String authToken;
     67         public final Account account;
     68 
     69         public AuthAccount(String user, String authToken, Account account) {
     70             this.user = user;
     71             this.authToken = authToken;
     72             this.account = account;
     73         }
     74     }
     75 
     76     public static Account[] getAccounts(Context context) {
     77         // Return the list of accounts supporting the Picasa GData service.
     78         AccountManager accountManager = AccountManager.get(context);
     79         Account[] accounts = {};
     80         try {
     81             accounts = accountManager.getAccountsByTypeAndFeatures(PicasaService.ACCOUNT_TYPE,
     82                     new String[] { PicasaService.FEATURE_SERVICE_NAME }, null, null).getResult();
     83         } catch (OperationCanceledException e) {
     84         } catch (AuthenticatorException e) {
     85         } catch (IOException e) {
     86         } catch (Exception e) {
     87             ;
     88         }
     89         return accounts;
     90     }
     91 
     92     public static AuthAccount[] getAuthenticatedAccounts(Context context) {
     93         AccountManager accountManager = AccountManager.get(context);
     94         Account[] accounts = getAccounts(context);
     95         if (accounts == null)
     96             accounts = new Account[0];
     97         int numAccounts = accounts.length;
     98 
     99         ArrayList<AuthAccount> authAccounts = new ArrayList<AuthAccount>(numAccounts);
    100         for (int i = 0; i != numAccounts; ++i) {
    101             Account account = accounts[i];
    102             String authToken;
    103             try {
    104                 // Get the token without user interaction.
    105                 authToken = accountManager.blockingGetAuthToken(account, PicasaService.SERVICE_NAME, true);
    106 
    107                 // TODO: Remove this once the build is signed by Google, since
    108                 // we will always have permission.
    109                 // This code requests permission from the user explicitly.
    110                 if (context instanceof Activity) {
    111                     Bundle bundle = accountManager.getAuthToken(account, PicasaService.SERVICE_NAME, null, (Activity) context,
    112                             null, null).getResult();
    113                     authToken = bundle.getString("authtoken");
    114                     PicasaService.requestSync(context, PicasaService.TYPE_USERS_ALBUMS, -1);
    115                 }
    116 
    117                 // Add the account information to the list of accounts.
    118                 if (authToken != null) {
    119                     String username = canonicalizeUsername(account.name);
    120                     authAccounts.add(new AuthAccount(username, authToken, account));
    121                 }
    122             } catch (OperationCanceledException e) {
    123             } catch (IOException e) {
    124             } catch (AuthenticatorException e) {
    125             } catch (Exception e) {
    126                 ;
    127             }
    128         }
    129         AuthAccount[] authArray = new AuthAccount[authAccounts.size()];
    130         authAccounts.toArray(authArray);
    131         return authArray;
    132     }
    133 
    134     /**
    135      * Returns a canonical username for a Gmail account.  Lowercases the username and
    136      * strips off a "gmail.com" or "googlemail.com" domain, but leaves other domains alone.
    137      *
    138      * e.g., Passing in "User (at) gmail.com: will return "user".
    139      *
    140      * @param username The username to be canonicalized.
    141      * @return The username, lowercased and possibly stripped of its domain if a "gmail.com" or
    142      * "googlemail.com" domain.
    143      */
    144     public static String canonicalizeUsername(String username) {
    145         username = username.toLowerCase();
    146         if (username.contains("@gmail.") || username.contains("@googlemail.")) {
    147             // Strip the domain from GMail accounts for
    148             // canonicalization. TODO: is there an official way?
    149             username = username.substring(0, username.indexOf('@'));
    150         }
    151         return username;
    152     }
    153 
    154     public PicasaApi() {
    155         mClient = new GDataClient();
    156     }
    157 
    158     public void setAuth(AuthAccount auth) {
    159         mAuth = auth;
    160         synchronized (mClient) {
    161             mClient.setAuthToken(auth.authToken);
    162         }
    163     }
    164 
    165     public int getAlbums(AccountManager accountManager, SyncResult syncResult, UserEntry user, GDataParser.EntryHandler handler) {
    166         // Construct the query URL for user albums.
    167         StringBuilder builder = new StringBuilder(BASE_URL);
    168         builder.append("user/");
    169         builder.append(Uri.encode(mAuth.user));
    170         builder.append(BASE_QUERY_STRING);
    171         builder.append("&kind=album");
    172         try {
    173             // Send the request.
    174             synchronized (mOperation) {
    175                 GDataClient.Operation operation = mOperation;
    176                 operation.inOutEtag = user.albumsEtag;
    177                 boolean retry = false;
    178                 int numRetries = 1;
    179                 do {
    180                     retry = false;
    181                     synchronized (mClient) {
    182                         mClient.get(builder.toString(), operation);
    183                     }
    184                     switch (operation.outStatus) {
    185                     case HttpStatus.SC_OK:
    186                         break;
    187                     case HttpStatus.SC_NOT_MODIFIED:
    188                         return RESULT_NOT_MODIFIED;
    189                     case HttpStatus.SC_FORBIDDEN:
    190                     case HttpStatus.SC_UNAUTHORIZED:
    191                         if (!retry) {
    192                             accountManager.invalidateAuthToken(PicasaService.ACCOUNT_TYPE, mAuth.authToken);
    193                             retry = true;
    194                         }
    195                         if (numRetries == 0) {
    196                             ++syncResult.stats.numAuthExceptions;
    197                         }
    198                     default:
    199                         Log.i(TAG, "getAlbums uri " + builder.toString());
    200                         Log.e(TAG, "getAlbums: unexpected status code " + operation.outStatus + " data: "
    201                                 + operation.outBody.toString());
    202                         ++syncResult.stats.numIoExceptions;
    203                         return RESULT_ERROR;
    204                     }
    205                     --numRetries;
    206                 } while (retry && numRetries >= 0);
    207 
    208                 // Store the new ETag for the user/albums feed.
    209                 user.albumsEtag = operation.inOutEtag;
    210 
    211                 // Parse the response.
    212                 synchronized (mParser) {
    213                     GDataParser parser = mParser;
    214                     parser.setEntry(mAlbumInstance);
    215                     parser.setHandler(handler);
    216                     try {
    217                         Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser);
    218                     } catch (SocketException e) {
    219                         Log.e(TAG, "getAlbumPhotos: " + e);
    220                         ++syncResult.stats.numIoExceptions;
    221                         e.printStackTrace();
    222                         return RESULT_ERROR;
    223                     }
    224                 }
    225             }
    226             return RESULT_OK;
    227         } catch (IOException e) {
    228             Log.e(TAG, "getAlbums: " + e);
    229             ++syncResult.stats.numIoExceptions;
    230         } catch (SAXException e) {
    231             Log.e(TAG, "getAlbums: " + e);
    232             ++syncResult.stats.numParseExceptions;
    233         }
    234         return RESULT_ERROR;
    235     }
    236 
    237     public int getAlbumPhotos(AccountManager accountManager, SyncResult syncResult, AlbumEntry album,
    238             GDataParser.EntryHandler handler) {
    239         // Construct the query URL for user albums.
    240         StringBuilder builder = new StringBuilder(BASE_URL);
    241         builder.append("user/");
    242         builder.append(Uri.encode(mAuth.user));
    243         builder.append("/albumid/");
    244         builder.append(album.id);
    245         builder.append(BASE_QUERY_STRING);
    246         builder.append("&kind=photo");
    247         try {
    248             // Send the request.
    249             synchronized (mOperation) {
    250                 GDataClient.Operation operation = mOperation;
    251                 operation.inOutEtag = album.photosEtag;
    252                 boolean retry = false;
    253                 int numRetries = 1;
    254                 do {
    255                     retry = false;
    256                     synchronized (mClient) {
    257                         mClient.get(builder.toString(), operation);
    258                     }
    259                     switch (operation.outStatus) {
    260                     case HttpStatus.SC_OK:
    261                         break;
    262                     case HttpStatus.SC_NOT_MODIFIED:
    263                         return RESULT_NOT_MODIFIED;
    264                     case HttpStatus.SC_FORBIDDEN:
    265                     case HttpStatus.SC_UNAUTHORIZED:
    266                         // We need to reset the authtoken and retry only once.
    267                         if (!retry) {
    268                             retry = true;
    269                             accountManager.invalidateAuthToken(PicasaService.SERVICE_NAME, mAuth.authToken);
    270                         }
    271                         if (numRetries == 0) {
    272                             ++syncResult.stats.numAuthExceptions;
    273                         }
    274                         break;
    275                     default:
    276                         Log.e(TAG, "getAlbumPhotos: " + builder.toString() + ", unexpected status code " + operation.outStatus);
    277                         ++syncResult.stats.numIoExceptions;
    278                         return RESULT_ERROR;
    279                     }
    280                     --numRetries;
    281                 } while (retry && numRetries >= 0);
    282 
    283                 // Store the new ETag for the album/photos feed.
    284                 album.photosEtag = operation.inOutEtag;
    285 
    286                 // Parse the response.
    287                 synchronized (mParser) {
    288                     GDataParser parser = mParser;
    289                     parser.setEntry(mPhotoInstance);
    290                     parser.setHandler(handler);
    291                     try {
    292                         Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser);
    293                     } catch (SocketException e) {
    294                         Log.e(TAG, "getAlbumPhotos: " + e);
    295                         ++syncResult.stats.numIoExceptions;
    296                         e.printStackTrace();
    297                         return RESULT_ERROR;
    298                     }
    299                 }
    300             }
    301             return RESULT_OK;
    302         } catch (IOException e) {
    303             Log.e(TAG, "getAlbumPhotos: " + e);
    304             ++syncResult.stats.numIoExceptions;
    305             e.printStackTrace();
    306         } catch (SAXException e) {
    307             Log.e(TAG, "getAlbumPhotos: " + e);
    308             ++syncResult.stats.numParseExceptions;
    309             e.printStackTrace();
    310         }
    311         return RESULT_ERROR;
    312     }
    313 
    314     public int deleteEntry(String editUri) {
    315         try {
    316             synchronized (mOperation) {
    317                 GDataClient.Operation operation = mOperation;
    318                 operation.inOutEtag = null;
    319                 synchronized (mClient) {
    320                     mClient.delete(editUri, operation);
    321                 }
    322                 if (operation.outStatus == 200) {
    323                     return RESULT_OK;
    324                 } else {
    325                     Log.e(TAG, "deleteEntry: failed with status code " + operation.outStatus);
    326                 }
    327             }
    328         } catch (IOException e) {
    329             Log.e(TAG, "deleteEntry: " + e);
    330         }
    331         return RESULT_ERROR;
    332     }
    333 
    334     /**
    335      * Column names shared by multiple entry kinds.
    336      */
    337     public static class Columns {
    338         public static final String _ID = "_id";
    339         public static final String SYNC_ACCOUNT = "sync_account";
    340         public static final String EDIT_URI = "edit_uri";
    341         public static final String TITLE = "title";
    342         public static final String SUMMARY = "summary";
    343         public static final String DATE_PUBLISHED = "date_published";
    344         public static final String DATE_UPDATED = "date_updated";
    345         public static final String DATE_EDITED = "date_edited";
    346         public static final String THUMBNAIL_URL = "thumbnail_url";
    347         public static final String HTML_PAGE_URL = "html_page_url";
    348     }
    349 }
    350