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