1 package com.cooliris.picasa;
3 import java.io.IOException;
4 import java.net.SocketException;
5 import java.util.ArrayList;
7 import org.apache.http.HttpStatus;
8 import org.xml.sax.SAXException;
10 import android.accounts.Account;
11 import android.accounts.AccountManager;
12 import android.accounts.AuthenticatorException;
13 import android.accounts.OperationCanceledException;
14 import android.app.Activity;
15 import android.content.Context;
16 import android.content.SyncResult;
17 import android.net.Uri;
18 import android.os.Bundle;
19 import android.util.Log;
20 import android.util.Xml;
22 public final class PicasaApi {
23 public static final int RESULT_OK = 0;
24 public static final int RESULT_NOT_MODIFIED = 1;
25 public static final int RESULT_ERROR = 2;
27 private static final String TAG = "PicasaAPI";
28 private static final String BASE_URL = "http://picasaweb.google.com/data/feed/api/";
29 private static final String BASE_QUERY_STRING;
32 // Build the base query string using screen dimensions.
33 final StringBuilder query = new StringBuilder("?imgmax=1024&max-results=1000&thumbsize=");
34 final String thumbnailSize = "144u,";
35 final String screennailSize = "1024u";
36 query.append(thumbnailSize);
37 query.append(screennailSize);
38 BASE_QUERY_STRING = query.toString() + "&visibility=visible";
41 private final GDataClient mClient;
42 private final GDataClient.Operation mOperation = new GDataClient.Operation();
43 private final GDataParser mParser = new GDataParser();
44 private final AlbumEntry mAlbumInstance = new AlbumEntry();
45 private final PhotoEntry mPhotoInstance = new PhotoEntry();
46 private AuthAccount mAuth;
48 public static final class AuthAccount {
49 public final String user;
50 public final String authToken;
51 public final Account account;
53 public AuthAccount(String user, String authToken, Account account) {
55 this.authToken = authToken;
56 this.account = account;
60 public static Account[] getAccounts(Context context) {
61 // Return the list of accounts supporting the Picasa GData service.
62 AccountManager accountManager = AccountManager.get(context);
63 Account[] accounts = {};
65 accounts = accountManager.getAccountsByTypeAndFeatures(PicasaService.ACCOUNT_TYPE,
66 new String[] { PicasaService.FEATURE_SERVICE_NAME }, null, null).getResult();
67 } catch (OperationCanceledException e) {
68 } catch (AuthenticatorException e) {
69 } catch (IOException e) {
74 public static AuthAccount[] getAuthenticatedAccounts(Context context) {
75 AccountManager accountManager = AccountManager.get(context);
76 Account[] accounts = getAccounts(context);
77 int numAccounts = accounts.length;
79 ArrayList<AuthAccount> authAccounts = new ArrayList<AuthAccount>(numAccounts);
80 for (int i = 0; i != numAccounts; ++i) {
81 Account account = accounts[i];
84 // Get the token without user interaction.
85 authToken = accountManager.blockingGetAuthToken(account, PicasaService.SERVICE_NAME, true);
87 // TODO: Remove this once the build is signed by Google, since
88 // we will always have permission.
89 // This code requests permission from the user explicitly.
90 if (context instanceof Activity) {
91 Bundle bundle = accountManager.getAuthToken(account, PicasaService.SERVICE_NAME, null, (Activity) context,
92 null, null).getResult();
93 authToken = bundle.getString("authtoken");
94 PicasaService.requestSync(context, PicasaService.TYPE_USERS_ALBUMS, -1);
97 // Add the account information to the list of accounts.
98 if (authToken != null) {
99 String username = account.name;
100 if (username.contains("@gmail.") || username.contains("@googlemail.")) {
101 // Strip the domain from GMail accounts for
102 // canonicalization. TODO: is there an official way?
103 username = username.substring(0, username.indexOf('@'));
105 authAccounts.add(new AuthAccount(username, authToken, account));
107 } catch (OperationCanceledException e) {
108 } catch (IOException e) {
109 } catch (AuthenticatorException e) {
112 AuthAccount[] authArray = new AuthAccount[authAccounts.size()];
113 authAccounts.toArray(authArray);
118 mClient = new GDataClient();
121 public void setAuth(AuthAccount auth) {
123 synchronized (mClient) {
124 mClient.setAuthToken(auth.authToken);
128 public int getAlbums(AccountManager accountManager, SyncResult syncResult, UserEntry user, GDataParser.EntryHandler handler) {
129 // Construct the query URL for user albums.
130 StringBuilder builder = new StringBuilder(BASE_URL);
131 builder.append("user/");
132 builder.append(Uri.encode(mAuth.user));
133 builder.append(BASE_QUERY_STRING);
134 builder.append("&kind=album");
137 synchronized (mOperation) {
138 GDataClient.Operation operation = mOperation;
139 operation.inOutEtag = user.albumsEtag;
140 boolean retry = false;
144 synchronized (mClient) {
145 mClient.get(builder.toString(), operation);
147 switch (operation.outStatus) {
148 case HttpStatus.SC_OK:
150 case HttpStatus.SC_NOT_MODIFIED:
151 return RESULT_NOT_MODIFIED;
152 case HttpStatus.SC_FORBIDDEN:
153 case HttpStatus.SC_UNAUTHORIZED:
155 accountManager.invalidateAuthToken(PicasaService.ACCOUNT_TYPE, mAuth.authToken);
158 if (numRetries == 0) {
159 ++syncResult.stats.numAuthExceptions;
162 Log.i(TAG, "getAlbums uri " + builder.toString());
163 Log.e(TAG, "getAlbums: unexpected status code " + operation.outStatus + " data: "
164 + operation.outBody.toString());
165 ++syncResult.stats.numIoExceptions;
169 } while (retry && numRetries >= 0);
171 // Store the new ETag for the user/albums feed.
172 user.albumsEtag = operation.inOutEtag;
174 // Parse the response.
175 synchronized (mParser) {
176 GDataParser parser = mParser;
177 parser.setEntry(mAlbumInstance);
178 parser.setHandler(handler);
180 Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser);
181 } catch (SocketException e) {
182 Log.e(TAG, "getAlbumPhotos: " + e);
183 ++syncResult.stats.numIoExceptions;
189 } catch (IOException e) {
190 Log.e(TAG, "getAlbums: " + e);
191 ++syncResult.stats.numIoExceptions;
192 } catch (SAXException e) {
193 Log.e(TAG, "getAlbums: " + e);
194 ++syncResult.stats.numParseExceptions;
199 public int getAlbumPhotos(AccountManager accountManager, SyncResult syncResult, AlbumEntry album,
200 GDataParser.EntryHandler handler) {
201 // Construct the query URL for user albums.
202 StringBuilder builder = new StringBuilder(BASE_URL);
203 builder.append("user/");
204 builder.append(Uri.encode(mAuth.user));
205 builder.append("/albumid/");
206 builder.append(album.id);
207 builder.append(BASE_QUERY_STRING);
208 builder.append("&kind=photo");
211 synchronized (mOperation) {
212 GDataClient.Operation operation = mOperation;
213 operation.inOutEtag = album.photosEtag;
214 boolean retry = false;
218 synchronized (mClient) {
219 mClient.get(builder.toString(), operation);
221 switch (operation.outStatus) {
222 case HttpStatus.SC_OK:
224 case HttpStatus.SC_NOT_MODIFIED:
225 return RESULT_NOT_MODIFIED;
226 case HttpStatus.SC_FORBIDDEN:
227 case HttpStatus.SC_UNAUTHORIZED:
228 // We need to reset the authtoken and retry only once.
231 accountManager.invalidateAuthToken(PicasaService.SERVICE_NAME, mAuth.authToken);
233 if (numRetries == 0) {
234 ++syncResult.stats.numAuthExceptions;
238 Log.e(TAG, "getAlbumPhotos: " + builder.toString() + ", unexpected status code " + operation.outStatus);
239 ++syncResult.stats.numIoExceptions;
243 } while (retry && numRetries >= 0);
245 // Store the new ETag for the album/photos feed.
246 album.photosEtag = operation.inOutEtag;
248 // Parse the response.
249 synchronized (mParser) {
250 GDataParser parser = mParser;
251 parser.setEntry(mPhotoInstance);
252 parser.setHandler(handler);
254 Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser);
255 } catch (SocketException e) {
256 Log.e(TAG, "getAlbumPhotos: " + e);
257 ++syncResult.stats.numIoExceptions;
263 } catch (IOException e) {
264 Log.e(TAG, "getAlbumPhotos: " + e);
265 ++syncResult.stats.numIoExceptions;
267 } catch (SAXException e) {
268 Log.e(TAG, "getAlbumPhotos: " + e);
269 ++syncResult.stats.numParseExceptions;
275 public int deleteEntry(String editUri) {
277 synchronized (mOperation) {
278 GDataClient.Operation operation = mOperation;
279 operation.inOutEtag = null;
280 synchronized (mClient) {
281 mClient.delete(editUri, operation);
283 if (operation.outStatus == 200) {
286 Log.e(TAG, "deleteEntry: failed with status code " + operation.outStatus);
289 } catch (IOException e) {
290 Log.e(TAG, "deleteEntry: " + e);
296 * Column names shared by multiple entry kinds.
298 public static class Columns {
299 public static final String _ID = "_id";
300 public static final String SYNC_ACCOUNT = "sync_account";
301 public static final String EDIT_URI = "edit_uri";
302 public static final String TITLE = "title";
303 public static final String SUMMARY = "summary";
304 public static final String DATE_PUBLISHED = "date_published";
305 public static final String DATE_UPDATED = "date_updated";
306 public static final String DATE_EDITED = "date_edited";
307 public static final String THUMBNAIL_URL = "thumbnail_url";
308 public static final String HTML_PAGE_URL = "html_page_url";