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