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 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