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 mUploadsAlbumName; 71 private final String mUnknownAlbumName; 72 private final LinkedList<ImageData> mRecycleBin; 73 private final ConnectivityManager mConnectivityManager; 74 private final int mMaxRecycleSize; 75 76 private Set<String> mFoundAlbumIds; 77 private int mLastPosition; 78 private int mDisplayLongSide; 79 80 public PicasaSource(Context context, SharedPreferences settings) { 81 super(context, settings); 82 mSourceName = TAG; 83 mLastPosition = INVALID; 84 mMaxPostAblums = mResources.getInteger(R.integer.max_post_albums); 85 mPostsAlbumName = mResources.getString(R.string.posts_album_name, "Posts"); 86 mUploadsAlbumName = mResources.getString(R.string.uploads_album_name, "Instant Uploads"); 87 mUnknownAlbumName = mResources.getString(R.string.unknown_album_name, "Unknown"); 88 mMaxRecycleSize = mResources.getInteger(R.integer.recycle_image_pool_size); 89 mConnectivityManager = 90 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 91 mRecycleBin = new LinkedList<ImageData>(); 92 93 fillQueue(); 94 mDisplayLongSide = getDisplayLongSide(); 95 } 96 97 private int getDisplayLongSide() { 98 DisplayMetrics metrics = new DisplayMetrics(); 99 WindowManager wm = (WindowManager) 100 mContext.getSystemService(Context.WINDOW_SERVICE); 101 wm.getDefaultDisplay().getMetrics(metrics); 102 return Math.max(metrics.heightPixels, metrics.widthPixels); 103 } 104 105 @Override 106 protected void openCursor(ImageData data) { 107 log(TAG, "opening single album"); 108 109 String[] projection = {PICASA_ID, PICASA_URL, PICASA_ROTATION, PICASA_ALBUM_ID}; 110 String selection = PICASA_ALBUM_ID + " = '" + data.albumId + "'"; 111 112 Uri.Builder picasaUriBuilder = new Uri.Builder() 113 .scheme("content") 114 .authority(PICASA_AUTHORITY) 115 .appendPath(PICASA_PHOTO_PATH); 116 data.cursor = mResolver.query(picasaUriBuilder.build(), 117 projection, selection, null, null); 118 } 119 120 @Override 121 protected void findPosition(ImageData data) { 122 if (data.position == UNINITIALIZED) { 123 if (data.cursor == null) { 124 openCursor(data); 125 } 126 if (data.cursor != null) { 127 int idIndex = data.cursor.getColumnIndex(PICASA_ID); 128 data.cursor.moveToPosition(-1); 129 while (data.position == -1 && data.cursor.moveToNext()) { 130 String id = data.cursor.getString(idIndex); 131 if (id != null && id.equals(data.id)) { 132 data.position = data.cursor.getPosition(); 133 } 134 } 135 if (data.position == -1) { 136 // oops! The image isn't in this album. How did we get here? 137 data.position = INVALID; 138 } 139 } 140 } 141 } 142 143 @Override 144 protected ImageData unpackImageData(Cursor cursor, ImageData data) { 145 if (data == null) { 146 data = new ImageData(); 147 } 148 int idIndex = cursor.getColumnIndex(PICASA_ID); 149 int urlIndex = cursor.getColumnIndex(PICASA_URL); 150 int bucketIndex = cursor.getColumnIndex(PICASA_ALBUM_ID); 151 152 data.id = cursor.getString(idIndex); 153 if (bucketIndex >= 0) { 154 data.albumId = cursor.getString(bucketIndex); 155 } 156 if (urlIndex >= 0) { 157 data.url = cursor.getString(urlIndex); 158 } 159 data.position = UNINITIALIZED; 160 data.cursor = null; 161 return data; 162 } 163 164 @Override 165 protected Collection<ImageData> findImages(int howMany) { 166 log(TAG, "finding images"); 167 LinkedList<ImageData> foundImages = new LinkedList<ImageData>(); 168 if (mConnectivityManager.isActiveNetworkMetered()) { 169 howMany = Math.min(howMany, mMaxRecycleSize); 170 log(TAG, "METERED: " + howMany); 171 if (!mRecycleBin.isEmpty()) { 172 foundImages.addAll(mRecycleBin); 173 log(TAG, "recycled " + foundImages.size() + " items."); 174 return foundImages; 175 } 176 } 177 178 String[] projection = {PICASA_ID, PICASA_URL, PICASA_ROTATION, PICASA_ALBUM_ID}; 179 LinkedList<String> albumIds = new LinkedList<String>(); 180 for (String id : getFoundAlbums()) { 181 if (mSettings.isAlbumEnabled(id)) { 182 String[] parts = id.split(":"); 183 if (parts.length > 2) { 184 albumIds.addAll(resolveAlbumIds(id)); 185 } else { 186 albumIds.add(parts[1]); 187 } 188 } 189 } 190 191 if (albumIds.size() > mMaxPostAblums) { 192 Collections.shuffle(albumIds); 193 } 194 195 StringBuilder selection = new StringBuilder(); 196 int albumIdx = 0; 197 for (String albumId : albumIds) { 198 if (albumIdx < mMaxPostAblums) { 199 if (selection.length() > 0) { 200 selection.append(" OR "); 201 } 202 log(TAG, "adding: " + albumId); 203 selection.append(PICASA_ALBUM_ID + " = '" + albumId + "'"); 204 } else { 205 log(TAG, "too many albums, dropping: " + albumId); 206 } 207 albumIdx++; 208 } 209 210 if (selection.length() == 0) { 211 return foundImages; 212 } 213 214 log(TAG, "selection is (" + selection.length() + "): " + selection.toString()); 215 216 Uri.Builder picasaUriBuilder = new Uri.Builder() 217 .scheme("content") 218 .authority(PICASA_AUTHORITY) 219 .appendPath(PICASA_PHOTO_PATH); 220 Cursor cursor = mResolver.query(picasaUriBuilder.build(), 221 projection, selection.toString(), null, null); 222 if (cursor != null) { 223 if (cursor.getCount() > howMany && mLastPosition == INVALID) { 224 mLastPosition = pickRandomStart(cursor.getCount(), howMany); 225 } 226 227 log(TAG, "moving to position: " + mLastPosition); 228 cursor.moveToPosition(mLastPosition); 229 230 int idIndex = cursor.getColumnIndex(PICASA_ID); 231 232 if (idIndex < 0) { 233 log(TAG, "can't find the ID column!"); 234 } else { 235 while (cursor.moveToNext()) { 236 if (idIndex >= 0) { 237 ImageData data = unpackImageData(cursor, null); 238 foundImages.offer(data); 239 } 240 mLastPosition = cursor.getPosition(); 241 } 242 if (cursor.isAfterLast()) { 243 mLastPosition = -1; 244 } 245 if (cursor.isBeforeFirst()) { 246 mLastPosition = INVALID; 247 } 248 } 249 250 cursor.close(); 251 } else { 252 Log.w(TAG, "received a null cursor in findImages()"); 253 } 254 log(TAG, "found " + foundImages.size() + " items."); 255 return foundImages; 256 } 257 258 private String resolveAccount(String id) { 259 String displayName = "unknown"; 260 String[] projection = {PICASA_ACCOUNT}; 261 Uri.Builder picasaUriBuilder = new Uri.Builder() 262 .scheme("content") 263 .authority(PICASA_AUTHORITY) 264 .appendPath(PICASA_USER_PATH) 265 .appendPath(id); 266 Cursor cursor = mResolver.query(picasaUriBuilder.build(), 267 projection, null, null, null); 268 if (cursor != null) { 269 cursor.moveToFirst(); 270 int accountIndex = cursor.getColumnIndex(PICASA_ACCOUNT); 271 if (accountIndex >= 0) { 272 displayName = cursor.getString(accountIndex); 273 } 274 cursor.close(); 275 } else { 276 Log.w(TAG, "received a null cursor in resolveAccount()"); 277 } 278 return displayName; 279 } 280 281 private Collection<String> resolveAlbumIds(String id) { 282 LinkedList<String> albumIds = new LinkedList<String>(); 283 log(TAG, "resolving " + id); 284 285 String[] parts = id.split(":"); 286 if (parts.length < 3) { 287 return albumIds; 288 } 289 290 String[] projection = {PICASA_ID, PICASA_ALBUM_TYPE, PICASA_ALBUM_UPDATED, 291 PICASA_ALBUM_USER}; 292 String order = PICASA_ALBUM_UPDATED + " DESC"; 293 String selection = (PICASA_ALBUM_USER + " = '" + parts[2] + "' AND " + 294 PICASA_ALBUM_TYPE + " = '" + parts[1] + "'"); 295 Uri.Builder picasaUriBuilder = new Uri.Builder() 296 .scheme("content") 297 .authority(PICASA_AUTHORITY) 298 .appendPath(PICASA_ALBUM_PATH) 299 .appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_IMAGE_VALUE); 300 Cursor cursor = mResolver.query(picasaUriBuilder.build(), 301 projection, selection, null, order); 302 if (cursor != null) { 303 log(TAG, " " + id + " resolved to " + cursor.getCount() + " albums"); 304 cursor.moveToPosition(-1); 305 306 int idIndex = cursor.getColumnIndex(PICASA_ID); 307 308 if (idIndex < 0) { 309 log(TAG, "can't find the ID column!"); 310 } else { 311 while (cursor.moveToNext()) { 312 albumIds.add(cursor.getString(idIndex)); 313 } 314 } 315 cursor.close(); 316 } else { 317 Log.w(TAG, "received a null cursor in resolveAlbumIds()"); 318 } 319 return albumIds; 320 } 321 322 private Set<String> getFoundAlbums() { 323 if (mFoundAlbumIds == null) { 324 findAlbums(); 325 } 326 return mFoundAlbumIds; 327 } 328 329 @Override 330 public Collection<AlbumData> findAlbums() { 331 log(TAG, "finding albums"); 332 HashMap<String, AlbumData> foundAlbums = new HashMap<String, AlbumData>(); 333 HashMap<String, String> accounts = new HashMap<String, String>(); 334 String[] projection = {PICASA_ID, PICASA_TITLE, PICASA_THUMB, PICASA_ALBUM_TYPE, 335 PICASA_ALBUM_USER, PICASA_ALBUM_UPDATED}; 336 Uri.Builder picasaUriBuilder = new Uri.Builder() 337 .scheme("content") 338 .authority(PICASA_AUTHORITY) 339 .appendPath(PICASA_ALBUM_PATH) 340 .appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_IMAGE_VALUE); 341 Cursor cursor = mResolver.query(picasaUriBuilder.build(), 342 projection, null, null, null); 343 if (cursor != null) { 344 cursor.moveToPosition(-1); 345 346 int idIndex = cursor.getColumnIndex(PICASA_ID); 347 int thumbIndex = cursor.getColumnIndex(PICASA_THUMB); 348 int titleIndex = cursor.getColumnIndex(PICASA_TITLE); 349 int typeIndex = cursor.getColumnIndex(PICASA_ALBUM_TYPE); 350 int updatedIndex = cursor.getColumnIndex(PICASA_ALBUM_UPDATED); 351 int userIndex = cursor.getColumnIndex(PICASA_ALBUM_USER); 352 353 if (idIndex < 0) { 354 log(TAG, "can't find the ID column!"); 355 } else { 356 while (cursor.moveToNext()) { 357 String id = TAG + ":" + cursor.getString(idIndex); 358 String user = (userIndex >= 0 ? cursor.getString(userIndex) : "-1"); 359 String type = (typeIndex >= 0 ? cursor.getString(typeIndex) : "none"); 360 boolean isPosts = (typeIndex >= 0 && PICASA_POSTS_TYPE.equals(type)); 361 boolean isUpload = (typeIndex >= 0 && 362 (PICASA_UPLOAD_TYPE.equals(type) || PICASA_UPLOADAUTO_TYPE.equals(type))); 363 364 String account = accounts.get(user); 365 if (account == null) { 366 account = resolveAccount(user); 367 accounts.put(user, account); 368 } 369 370 if (isPosts) { 371 log(TAG, "replacing " + id + " with " + PICASA_POSTS_TYPE); 372 id = TAG + ":" + PICASA_POSTS_TYPE + ":" + user; 373 } 374 375 if (isUpload) { 376 log(TAG, "replacing " + id + " with " + PICASA_UPLOAD_TYPE); 377 id = TAG + ":" + PICASA_UPLOAD_TYPE + ":" + user; 378 } 379 380 String thumbnailUrl = null; 381 long updated = 0; 382 AlbumData data = foundAlbums.get(id); 383 if (data == null) { 384 data = new AlbumData(); 385 data.id = id; 386 data.account = account; 387 388 if (isPosts) { 389 data.title = mPostsAlbumName; 390 } else if (isUpload) { 391 data.title = mUploadsAlbumName; 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 @Override 429 protected InputStream getStream(ImageData data, int longSide) { 430 InputStream is = null; 431 try { 432 Uri.Builder photoUriBuilder = new Uri.Builder() 433 .scheme("content") 434 .authority(PICASA_AUTHORITY) 435 .appendPath(PICASA_PHOTO_PATH) 436 .appendPath(data.id); 437 if (mConnectivityManager.isActiveNetworkMetered() || 438 ((2 * longSide) <= mDisplayLongSide)) { 439 photoUriBuilder.appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_SCREEN_VALUE); 440 } else { 441 photoUriBuilder.appendQueryParameter(PICASA_TYPE_KEY, PICASA_TYPE_FULL_VALUE); 442 } 443 if (data.url != null) { 444 photoUriBuilder.appendQueryParameter(PICASA_URL_KEY, data.url); 445 } 446 is = mResolver.openInputStream(photoUriBuilder.build()); 447 } catch (FileNotFoundException fnf) { 448 log(TAG, "file not found: " + fnf); 449 is = null; 450 } 451 452 if (is != null) { 453 mRecycleBin.offer(data); 454 log(TAG, "RECYCLED"); 455 while (mRecycleBin.size() > mMaxRecycleSize) { 456 mRecycleBin.poll(); 457 } 458 } 459 return is; 460 } 461 } 462