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 
PicasaSource(Context context, SharedPreferences settings)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 
getDisplayLongSide()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
openCursor(ImageData data)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
findPosition(ImageData data)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
unpackImageData(Cursor cursor, ImageData data)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
findImages(int howMany)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 
resolveAccount(String id)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 
resolveAlbumIds(String id)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 
getFoundAlbums()320     private Set<String> getFoundAlbums() {
321         if (mFoundAlbumIds == null) {
322             findAlbums();
323         }
324         return mFoundAlbumIds;
325     }
326 
327     @Override
findAlbums()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 
constructId(String serverId)428     public static String constructId(String serverId) {
429         return  TAG + ":" + serverId;
430     }
431 
432     @Override
getStream(ImageData data, int longSide)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