Home | History | Annotate | Download | only in phototable
      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