package com.cooliris.picasa; import java.io.IOException; import java.net.SocketException; import java.util.ArrayList; import org.apache.http.HttpStatus; import org.xml.sax.SAXException; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.content.Context; import android.content.SyncResult; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.util.Xml; public final class PicasaApi { public static final int RESULT_OK = 0; public static final int RESULT_NOT_MODIFIED = 1; public static final int RESULT_ERROR = 2; private static final String TAG = "PicasaAPI"; private static final String BASE_URL = "http://picasaweb.google.com/data/feed/api/"; private static final String BASE_QUERY_STRING; static { // Build the base query string using screen dimensions. final StringBuilder query = new StringBuilder("?imgmax=1024&max-results=1000&thumbsize="); final String thumbnailSize = "144u,"; final String screennailSize = "1024u"; query.append(thumbnailSize); query.append(screennailSize); BASE_QUERY_STRING = query.toString() + "&visibility=visible"; } private final GDataClient mClient; private final GDataClient.Operation mOperation = new GDataClient.Operation(); private final GDataParser mParser = new GDataParser(); private final AlbumEntry mAlbumInstance = new AlbumEntry(); private final PhotoEntry mPhotoInstance = new PhotoEntry(); private AuthAccount mAuth; public static final class AuthAccount { public final String user; public final String authToken; public final Account account; public AuthAccount(String user, String authToken, Account account) { this.user = user; this.authToken = authToken; this.account = account; } } public static Account[] getAccounts(Context context) { // Return the list of accounts supporting the Picasa GData service. AccountManager accountManager = AccountManager.get(context); Account[] accounts = {}; try { accounts = accountManager.getAccountsByTypeAndFeatures(PicasaService.ACCOUNT_TYPE, new String[] { PicasaService.FEATURE_SERVICE_NAME }, null, null).getResult(); } catch (OperationCanceledException e) { } catch (AuthenticatorException e) { } catch (IOException e) { } return accounts; } public static AuthAccount[] getAuthenticatedAccounts(Context context) { AccountManager accountManager = AccountManager.get(context); Account[] accounts = getAccounts(context); int numAccounts = accounts.length; ArrayList<AuthAccount> authAccounts = new ArrayList<AuthAccount>(numAccounts); for (int i = 0; i != numAccounts; ++i) { Account account = accounts[i]; String authToken; try { // Get the token without user interaction. authToken = accountManager.blockingGetAuthToken(account, PicasaService.SERVICE_NAME, true); // TODO: Remove this once the build is signed by Google, since // we will always have permission. // This code requests permission from the user explicitly. if (context instanceof Activity) { Bundle bundle = accountManager.getAuthToken(account, PicasaService.SERVICE_NAME, null, (Activity) context, null, null).getResult(); authToken = bundle.getString("authtoken"); PicasaService.requestSync(context, PicasaService.TYPE_USERS_ALBUMS, -1); } // Add the account information to the list of accounts. if (authToken != null) { String username = account.name; if (username.contains("@gmail.") || username.contains("@googlemail.")) { // Strip the domain from GMail accounts for // canonicalization. TODO: is there an official way? username = username.substring(0, username.indexOf('@')); } authAccounts.add(new AuthAccount(username, authToken, account)); } } catch (OperationCanceledException e) { } catch (IOException e) { } catch (AuthenticatorException e) { } } AuthAccount[] authArray = new AuthAccount[authAccounts.size()]; authAccounts.toArray(authArray); return authArray; } public PicasaApi() { mClient = new GDataClient(); } public void setAuth(AuthAccount auth) { mAuth = auth; synchronized (mClient) { mClient.setAuthToken(auth.authToken); } } public int getAlbums(AccountManager accountManager, SyncResult syncResult, UserEntry user, GDataParser.EntryHandler handler) { // Construct the query URL for user albums. StringBuilder builder = new StringBuilder(BASE_URL); builder.append("user/"); builder.append(Uri.encode(mAuth.user)); builder.append(BASE_QUERY_STRING); builder.append("&kind=album"); try { // Send the request. synchronized (mOperation) { GDataClient.Operation operation = mOperation; operation.inOutEtag = user.albumsEtag; boolean retry = false; int numRetries = 1; do { retry = false; synchronized (mClient) { mClient.get(builder.toString(), operation); } switch (operation.outStatus) { case HttpStatus.SC_OK: break; case HttpStatus.SC_NOT_MODIFIED: return RESULT_NOT_MODIFIED; case HttpStatus.SC_FORBIDDEN: case HttpStatus.SC_UNAUTHORIZED: if (!retry) { accountManager.invalidateAuthToken(PicasaService.ACCOUNT_TYPE, mAuth.authToken); retry = true; } if (numRetries == 0) { ++syncResult.stats.numAuthExceptions; } default: Log.i(TAG, "getAlbums uri " + builder.toString()); Log.e(TAG, "getAlbums: unexpected status code " + operation.outStatus + " data: " + operation.outBody.toString()); ++syncResult.stats.numIoExceptions; return RESULT_ERROR; } --numRetries; } while (retry && numRetries >= 0); // Store the new ETag for the user/albums feed. user.albumsEtag = operation.inOutEtag; // Parse the response. synchronized (mParser) { GDataParser parser = mParser; parser.setEntry(mAlbumInstance); parser.setHandler(handler); try { Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser); } catch (SocketException e) { Log.e(TAG, "getAlbumPhotos: " + e); ++syncResult.stats.numIoExceptions; e.printStackTrace(); return RESULT_ERROR; } } } return RESULT_OK; } catch (IOException e) { Log.e(TAG, "getAlbums: " + e); ++syncResult.stats.numIoExceptions; } catch (SAXException e) { Log.e(TAG, "getAlbums: " + e); ++syncResult.stats.numParseExceptions; } return RESULT_ERROR; } public int getAlbumPhotos(AccountManager accountManager, SyncResult syncResult, AlbumEntry album, GDataParser.EntryHandler handler) { // Construct the query URL for user albums. StringBuilder builder = new StringBuilder(BASE_URL); builder.append("user/"); builder.append(Uri.encode(mAuth.user)); builder.append("/albumid/"); builder.append(album.id); builder.append(BASE_QUERY_STRING); builder.append("&kind=photo"); try { // Send the request. synchronized (mOperation) { GDataClient.Operation operation = mOperation; operation.inOutEtag = album.photosEtag; boolean retry = false; int numRetries = 1; do { retry = false; synchronized (mClient) { mClient.get(builder.toString(), operation); } switch (operation.outStatus) { case HttpStatus.SC_OK: break; case HttpStatus.SC_NOT_MODIFIED: return RESULT_NOT_MODIFIED; case HttpStatus.SC_FORBIDDEN: case HttpStatus.SC_UNAUTHORIZED: // We need to reset the authtoken and retry only once. if (!retry) { retry = true; accountManager.invalidateAuthToken(PicasaService.SERVICE_NAME, mAuth.authToken); } if (numRetries == 0) { ++syncResult.stats.numAuthExceptions; } break; default: Log.e(TAG, "getAlbumPhotos: " + builder.toString() + ", unexpected status code " + operation.outStatus); ++syncResult.stats.numIoExceptions; return RESULT_ERROR; } --numRetries; } while (retry && numRetries >= 0); // Store the new ETag for the album/photos feed. album.photosEtag = operation.inOutEtag; // Parse the response. synchronized (mParser) { GDataParser parser = mParser; parser.setEntry(mPhotoInstance); parser.setHandler(handler); try { Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser); } catch (SocketException e) { Log.e(TAG, "getAlbumPhotos: " + e); ++syncResult.stats.numIoExceptions; e.printStackTrace(); return RESULT_ERROR; } } } return RESULT_OK; } catch (IOException e) { Log.e(TAG, "getAlbumPhotos: " + e); ++syncResult.stats.numIoExceptions; e.printStackTrace(); } catch (SAXException e) { Log.e(TAG, "getAlbumPhotos: " + e); ++syncResult.stats.numParseExceptions; e.printStackTrace(); } return RESULT_ERROR; } public int deleteEntry(String editUri) { try { synchronized (mOperation) { GDataClient.Operation operation = mOperation; operation.inOutEtag = null; synchronized (mClient) { mClient.delete(editUri, operation); } if (operation.outStatus == 200) { return RESULT_OK; } else { Log.e(TAG, "deleteEntry: failed with status code " + operation.outStatus); } } } catch (IOException e) { Log.e(TAG, "deleteEntry: " + e); } return RESULT_ERROR; } /** * Column names shared by multiple entry kinds. */ public static class Columns { public static final String _ID = "_id"; public static final String SYNC_ACCOUNT = "sync_account"; public static final String EDIT_URI = "edit_uri"; public static final String TITLE = "title"; public static final String SUMMARY = "summary"; public static final String DATE_PUBLISHED = "date_published"; public static final String DATE_UPDATED = "date_updated"; public static final String DATE_EDITED = "date_edited"; public static final String THUMBNAIL_URL = "thumbnail_url"; public static final String HTML_PAGE_URL = "html_page_url"; } }