PicasaSource.java revision 729f5231bf95ec7946fc4c510d44db303b07614d
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 */
16package com.android.dreams.phototable;
17
18import android.content.Context;
19import android.content.SharedPreferences;
20import android.database.Cursor;
21import android.net.ConnectivityManager;
22import android.net.Uri;
23import android.util.DisplayMetrics;
24import android.util.Log;
25import android.view.WindowManager;
26
27import java.io.FileNotFoundException;
28import java.io.InputStream;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.HashMap;
32import java.util.LinkedList;
33import java.util.Set;
34
35/**
36 * Loads images from Picasa.
37 */
38public 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 = constructId(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 = constructId(PICASA_POSTS_TYPE + ":" + user);
373                    }
374
375                    if (isUpload) {
376                        log(TAG, "replacing " + id + " with " + PICASA_UPLOAD_TYPE);
377                        id = constructId(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    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