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