1 /* 2 * Copyright (C) 2012 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 package com.android.dreams.phototable; 17 18 import android.content.Context; 19 import android.content.SharedPreferences; 20 import android.database.Cursor; 21 import android.net.ConnectivityManager; 22 import android.net.Uri; 23 import android.util.DisplayMetrics; 24 import android.util.Log; 25 import android.view.WindowManager; 26 27 import java.io.FileNotFoundException; 28 import java.io.InputStream; 29 import java.util.Collection; 30 import java.util.Collections; 31 import java.util.HashMap; 32 import java.util.LinkedList; 33 import java.util.Set; 34 35 /** 36 * Loads images from Picasa. 37 */ 38 public class PicasaSource extends CursorPhotoSource { 39 private static final String TAG = "PhotoTable.PicasaSource"; 40 41 private static final String PICASA_AUTHORITY = 42 "com.google.android.gallery3d.GooglePhotoProvider"; 43 44 private static final String PICASA_PHOTO_PATH = "photos"; 45 private static final String PICASA_ALBUM_PATH = "albums"; 46 private static final String PICASA_USER_PATH = "users"; 47 48 private static final String PICASA_ID = "_id"; 49 private static final String PICASA_URL = "content_url"; 50 private static final String PICASA_ROTATION = "rotation"; 51 private static final String PICASA_ALBUM_ID = "album_id"; 52 private static final String PICASA_TITLE = "title"; 53 private static final String PICASA_THUMB = "thumbnail_url"; 54 private static final String PICASA_ALBUM_TYPE = "album_type"; 55 private static final String PICASA_ALBUM_USER = "user_id"; 56 private static final String PICASA_ALBUM_UPDATED = "date_updated"; 57 private static final String PICASA_ACCOUNT = "account"; 58 59 private static final String PICASA_URL_KEY = "content_url"; 60 private static final String PICASA_TYPE_KEY = "type"; 61 private static final String PICASA_TYPE_FULL_VALUE = "full"; 62 private static final String PICASA_TYPE_SCREEN_VALUE = "screennail"; 63 private static final String PICASA_TYPE_IMAGE_VALUE = "image"; 64 private static final String PICASA_POSTS_TYPE = "Buzz"; 65 private static final String PICASA_UPLOAD_TYPE = "InstantUpload"; 66 private static final String PICASA_UPLOADAUTO_TYPE = "InstantUploadAuto"; 67 68 private final int mMaxPostAblums; 69 private final String mPostsAlbumName; 70 private final String mUnknownAlbumName; 71 private final LinkedList<ImageData> mRecycleBin; 72 private final ConnectivityManager mConnectivityManager; 73 private final int mMaxRecycleSize; 74 75 private Set<String> mFoundAlbumIds; 76 private int mLastPosition; 77 private int mDisplayLongSide; 78 79 public PicasaSource(Context context, SharedPreferences settings) { 80 super(context, settings); 81 mSourceName = TAG; 82 mLastPosition = INVALID; 83 mMaxPostAblums = mResources.getInteger(R.integer.max_post_albums); 84 mPostsAlbumName = mResources.getString(R.string.posts_album_name, "Posts"); 85 mUnknownAlbumName = mResources.getString(R.string.unknown_album_name, "Unknown"); 86 mMaxRecycleSize = mResources.getInteger(R.integer.recycle_image_pool_size); 87 mConnectivityManager = 88 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 89 mRecycleBin = new LinkedList<ImageData>(); 90 91 fillQueue(); 92 mDisplayLongSide = getDisplayLongSide(); 93 } 94 95 private int getDisplayLongSide() { 96 DisplayMetrics metrics = new DisplayMetrics(); 97 WindowManager wm = (WindowManager) 98 mContext.getSystemService(Context.WINDOW_SERVICE); 99 wm.getDefaultDisplay().getMetrics(metrics); 100 return Math.max(metrics.heightPixels, metrics.widthPixels); 101 } 102 103 @Override 104 protected void openCursor(ImageData data) { 105 log(TAG, "opening single album"); 106 107 String[] projection = {PICASA_ID, PICASA_URL, PICASA_ROTATION, PICASA_ALBUM_ID}; 108 String selection = PICASA_ALBUM_ID + " = '" + data.albumId + "'"; 109 110 Uri.Builder picasaUriBuilder = new Uri.Builder() 111 .scheme("content") 112 .authority(PICASA_AUTHORITY) 113 .appendPath(PICASA_PHOTO_PATH); 114 data.cursor = mResolver.query(picasaUriBuilder.build(), 115 projection, selection, null, null); 116 } 117 118 @Override 119 protected void findPosition(ImageData data) { 120 if (data.position == UNINITIALIZED) { 121 if (data.cursor == null) { 122 openCursor(data); 123 } 124 if (data.cursor != null) { 125 int idIndex = data.cursor.getColumnIndex(PICASA_ID); 126 data.cursor.moveToPosition(-1); 127 while (data.position == -1 && data.cursor.moveToNext()) { 128 String id = data.cursor.getString(idIndex); 129 if (id != null && id.equals(data.id)) { 130 data.position = data.cursor.getPosition(); 131 } 132 } 133 if (data.position == -1) { 134 // oops! The image isn't in this album. How did we get here? 135 data.position = INVALID; 136 } 137 } 138 } 139 } 140 141 @Override 142 protected ImageData unpackImageData(Cursor cursor, ImageData data) { 143 if (data == null) { 144 data = new ImageData(); 145 } 146 int idIndex = cursor.getColumnIndex(PICASA_ID); 147 int urlIndex = cursor.getColumnIndex(PICASA_URL); 148 int bucketIndex = cursor.getColumnIndex(PICASA_ALBUM_ID); 149 150 data.id = cursor.getString(idIndex); 151 if (bucketIndex >= 0) { 152 data.albumId = cursor.getString(bucketIndex); 153 } 154 if (urlIndex >= 0) { 155 data.url = cursor.getString(urlIndex); 156 } 157 data.position = UNINITIALIZED; 158 data.cursor = null; 159 return data; 160 } 161 162 @Override 163 protected Collection<ImageData> findImages(int howMany) { 164 log(TAG, "finding images"); 165 LinkedList<ImageData> foundImages = new LinkedList<ImageData>(); 166 if (mConnectivityManager.isActiveNetworkMetered()) { 167 howMany = Math.min(howMany, mMaxRecycleSize); 168 log(TAG, "METERED: " + howMany); 169 if (!mRecycleBin.isEmpty()) { 170 foundImages.addAll(mRecycleBin); 171 log(TAG, "recycled " + foundImages.size() + " items."); 172 return foundImages; 173 } 174 } 175 176 String[] projection = {PICASA_ID, PICASA_URL, PICASA_ROTATION, PICASA_ALBUM_ID}; 177 LinkedList<String> albumIds = new LinkedList<String>(); 178 for (String id : getFoundAlbums()) { 179 if (mSettings.isAlbumEnabled(id)) { 180 String[] parts = id.split(":"); 181 if (parts.length > 2) { 182 albumIds.addAll(resolveAlbumIds(id)); 183 } else { 184 albumIds.add(parts[1]); 185 } 186 } 187 } 188 189 if (albumIds.size() > mMaxPostAblums) { 190 Collections.shuffle(albumIds); 191 } 192 193 StringBuilder selection = new StringBuilder(); 194 int albumIdx = 0; 195 for (String albumId : albumIds) { 196 if (albumIdx < mMaxPostAblums) { 197 if (selection.length() > 0) { 198 selection.append(" OR "); 199 } 200 log(TAG, "adding: " + albumId); 201 selection.append(PICASA_ALBUM_ID + " = '" + albumId + "'"); 202 } else { 203 log(TAG, "too many albums, dropping: " + albumId); 204 } 205 albumIdx++; 206 } 207 208 if (selection.length() == 0) { 209 return foundImages; 210 } 211 212 log(TAG, "selection is (" + selection.length() + "): " + selection.toString()); 213 214 Uri.Builder picasaUriBuilder = new Uri.Builder() 215 .scheme("content") 216 .authority(PICASA_AUTHORITY) 217 .appendPath(PICASA_PHOTO_PATH); 218 Cursor cursor = mResolver.query(picasaUriBuilder.build(), 219 projection, selection.toString(), null, null); 220 if (cursor != null) { 221 if (cursor.getCount() > howMany && mLastPosition == INVALID) { 222 mLastPosition = pickRandomStart(cursor.getCount(), howMany); 223 } 224 225 log(TAG, "moving to position: " + mLastPosition); 226 cursor.moveToPosition(mLastPosition); 227 228 int idIndex = cursor.getColumnIndex(PICASA_ID); 229 230 if (idIndex < 0) { 231 log(TAG, "can't find the ID column!"); 232 } else { 233 while (cursor.moveToNext()) { 234 if (idIndex >= 0) { 235 ImageData data = unpackImageData(cursor, null); 236 foundImages.offer(data); 237 } 238 mLastPosition = cursor.getPosition(); 239 } 240 if (cursor.isAfterLast()) { 241 mLastPosition = -1; 242 } 243 if (cursor.isBeforeFirst()) { 244 mLastPosition = INVALID; 245 } 246 } 247 248 cursor.close(); 249 } else { 250 Log.w(TAG, "received a null cursor in findImages()"); 251 } 252 log(TAG, "found " + foundImages.size() + " items."); 253 return foundImages; 254 } 255 256 private String resolveAccount(String id) { 257 String displayName = "unknown"; 258 String[] projection = {PICASA_ACCOUNT}; 259 Uri.Builder picasaUriBuilder = new Uri.Builder() 260 .scheme("content") 261 .authority(PICASA_AUTHORITY) 262 .appendPath(PICASA_USER_PATH) 263 .appendPath(id); 264 Cursor cursor = mResolver.query(picasaUriBuilder.build(), 265 projection, null, null, null); 266 if (cursor != null) { 267 cursor.moveToFirst(); 268 int accountIndex = cursor.getColumnIndex(PICASA_ACCOUNT); 269 if (accountIndex >= 0) { 270 displayName = cursor.getString(accountIndex); 271 } 272 cursor.close(); 273 } else { 274 Log.w(TAG, "received a null cursor in resolveAccount()"); 275 } 276 return displayName; 277 } 278 279 private Collection<String> resolveAlbumIds(String id) { 280 LinkedList<String> albumIds = new LinkedList<String>(); 281 log(TAG, "resolving " + id); 282 283 String[] parts = id.split(":"); 284 if (parts.length < 3) { 285 return albumIds; 286 } 287 288 String[] projection = {PICASA_ID, PICASA_ALBUM_TYPE, PICASA_ALBUM_UPDATED, 289 PICASA_ALBUM_USER}; 290 String order = PICASA_ALBUM_UPDATED + " DESC"; 291 String selection = (PICASA_ALBUM_USER + " = '" + parts[2] + "' AND " + 292 PICASA_ALBUM_TYPE + " = '" + parts[1] + "'"); 293 Uri.Builder picasaUriBuilder = new Uri.Builder() 294 .scheme("content") 295 .authority(PICASA_AUTHORITY) 296 .appendPath(PICASA_ALBUM_PATH) 297 .appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_IMAGE_VALUE); 298 Cursor cursor = mResolver.query(picasaUriBuilder.build(), 299 projection, selection, null, order); 300 if (cursor != null) { 301 log(TAG, " " + id + " resolved to " + cursor.getCount() + " albums"); 302 cursor.moveToPosition(-1); 303 304 int idIndex = cursor.getColumnIndex(PICASA_ID); 305 306 if (idIndex < 0) { 307 log(TAG, "can't find the ID column!"); 308 } else { 309 while (cursor.moveToNext()) { 310 albumIds.add(cursor.getString(idIndex)); 311 } 312 } 313 cursor.close(); 314 } else { 315 Log.w(TAG, "received a null cursor in resolveAlbumIds()"); 316 } 317 return albumIds; 318 } 319 320 private Set<String> getFoundAlbums() { 321 if (mFoundAlbumIds == null) { 322 findAlbums(); 323 } 324 return mFoundAlbumIds; 325 } 326 327 @Override 328 public Collection<AlbumData> findAlbums() { 329 log(TAG, "finding albums"); 330 HashMap<String, AlbumData> foundAlbums = new HashMap<String, AlbumData>(); 331 HashMap<String, String> accounts = new HashMap<String, String>(); 332 String[] projection = {PICASA_ID, PICASA_TITLE, PICASA_THUMB, PICASA_ALBUM_TYPE, 333 PICASA_ALBUM_USER, PICASA_ALBUM_UPDATED}; 334 Uri.Builder picasaUriBuilder = new Uri.Builder() 335 .scheme("content") 336 .authority(PICASA_AUTHORITY) 337 .appendPath(PICASA_ALBUM_PATH) 338 .appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_IMAGE_VALUE); 339 Cursor cursor = mResolver.query(picasaUriBuilder.build(), 340 projection, null, null, null); 341 if (cursor != null) { 342 cursor.moveToPosition(-1); 343 344 int idIndex = cursor.getColumnIndex(PICASA_ID); 345 int thumbIndex = cursor.getColumnIndex(PICASA_THUMB); 346 int titleIndex = cursor.getColumnIndex(PICASA_TITLE); 347 int typeIndex = cursor.getColumnIndex(PICASA_ALBUM_TYPE); 348 int updatedIndex = cursor.getColumnIndex(PICASA_ALBUM_UPDATED); 349 int userIndex = cursor.getColumnIndex(PICASA_ALBUM_USER); 350 351 if (idIndex < 0) { 352 log(TAG, "can't find the ID column!"); 353 } else { 354 while (cursor.moveToNext()) { 355 String id = constructId(cursor.getString(idIndex)); 356 String user = (userIndex >= 0 ? cursor.getString(userIndex) : "-1"); 357 String type = (typeIndex >= 0 ? cursor.getString(typeIndex) : "none"); 358 boolean isPosts = (typeIndex >= 0 && PICASA_POSTS_TYPE.equals(type)); 359 boolean isUpload = (typeIndex >= 0 && 360 (PICASA_UPLOAD_TYPE.equals(type) || PICASA_UPLOADAUTO_TYPE.equals(type))); 361 362 String account = accounts.get(user); 363 if (account == null) { 364 account = resolveAccount(user); 365 accounts.put(user, account); 366 } 367 368 if (isPosts) { 369 log(TAG, "replacing " + id + " with " + PICASA_POSTS_TYPE); 370 id = constructId(PICASA_POSTS_TYPE + ":" + user); 371 } 372 373 if (isUpload) { 374 // check for old-style name for this album, and upgrade settings. 375 String uploadId = constructId(PICASA_UPLOAD_TYPE + ":" + user); 376 if (mSettings.isAlbumEnabled(uploadId)) { 377 mSettings.setAlbumEnabled(uploadId, false); 378 mSettings.setAlbumEnabled(id, true); 379 } 380 } 381 382 String thumbnailUrl = null; 383 long updated = 0; 384 AlbumData data = foundAlbums.get(id); 385 if (data == null) { 386 data = new AlbumData(); 387 data.id = id; 388 data.account = account; 389 390 if (isPosts) { 391 data.title = mPostsAlbumName; 392 } else if (titleIndex >= 0) { 393 data.title = cursor.getString(titleIndex); 394 } else { 395 data.title = mUnknownAlbumName; 396 } 397 398 log(TAG, "found " + data.title + "(" + data.id + ")" + 399 " of type " + type + " owned by " + user); 400 foundAlbums.put(id, data); 401 } 402 403 if (updatedIndex >= 0) { 404 updated = cursor.getLong(updatedIndex); 405 } 406 407 if (thumbIndex >= 0) { 408 thumbnailUrl = cursor.getString(thumbIndex); 409 } 410 411 data.updated = (long) Math.max(data.updated, updated); 412 413 if (data.thumbnailUrl == null || data.updated == updated) { 414 data.thumbnailUrl = thumbnailUrl; 415 } 416 } 417 } 418 cursor.close(); 419 420 } else { 421 Log.w(TAG, "received a null cursor in findAlbums()"); 422 } 423 log(TAG, "found " + foundAlbums.size() + " items."); 424 mFoundAlbumIds = foundAlbums.keySet(); 425 return foundAlbums.values(); 426 } 427 428 public static String constructId(String serverId) { 429 return TAG + ":" + serverId; 430 } 431 432 @Override 433 protected InputStream getStream(ImageData data, int longSide) { 434 InputStream is = null; 435 try { 436 Uri.Builder photoUriBuilder = new Uri.Builder() 437 .scheme("content") 438 .authority(PICASA_AUTHORITY) 439 .appendPath(PICASA_PHOTO_PATH) 440 .appendPath(data.id); 441 if (mConnectivityManager.isActiveNetworkMetered() || 442 ((2 * longSide) <= mDisplayLongSide)) { 443 photoUriBuilder.appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_SCREEN_VALUE); 444 } else { 445 photoUriBuilder.appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_FULL_VALUE); 446 } 447 if (data.url != null) { 448 photoUriBuilder.appendQueryParameter(PICASA_URL_KEY, data.url); 449 } 450 is = mResolver.openInputStream(photoUriBuilder.build()); 451 } catch (FileNotFoundException fnf) { 452 log(TAG, "file not found: " + fnf); 453 is = null; 454 } 455 456 if (is != null) { 457 mRecycleBin.offer(data); 458 log(TAG, "RECYCLED"); 459 while (mRecycleBin.size() > mMaxRecycleSize) { 460 mRecycleBin.poll(); 461 } 462 } 463 return is; 464 } 465 } 466