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.ContentResolver;
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.content.res.Resources;
22import android.database.Cursor;
23import android.graphics.Bitmap;
24import android.graphics.BitmapFactory;
25import android.graphics.Matrix;
26import android.net.Uri;
27import android.util.Log;
28
29import java.io.BufferedInputStream;
30import java.io.FileNotFoundException;
31import java.io.IOException;
32import java.io.InputStream;
33import java.util.Collection;
34import java.util.Collections;
35import java.util.HashMap;
36import java.util.LinkedList;
37import java.util.Random;
38
39/**
40 * Picks a random image from a source of photos.
41 */
42public abstract class PhotoSource {
43    private static final String TAG = "PhotoTable.PhotoSource";
44    private static final boolean DEBUG = false;
45
46    // This should be large enough for BitmapFactory to decode the header so
47    // that we can mark and reset the input stream to avoid duplicate network i/o
48    private static final int BUFFER_SIZE = 32 * 1024;
49
50    public class ImageData {
51        public String id;
52        public String url;
53        public int orientation;
54
55        protected String albumId;
56        protected Cursor cursor;
57        protected int position;
58        protected Uri uri;
59
60        InputStream getStream(int longSide) {
61            return PhotoSource.this.getStream(this, longSide);
62        }
63        ImageData naturalNext() {
64            return PhotoSource.this.naturalNext(this);
65        }
66        ImageData naturalPrevious() {
67            return PhotoSource.this.naturalPrevious(this);
68        }
69        public void donePaging() {
70            PhotoSource.this.donePaging(this);
71        }
72    }
73
74    public class AlbumData {
75        public String id;
76        public String title;
77        public String thumbnailUrl;
78        public String account;
79        public long updated;
80
81        public String getType() {
82            String type = PhotoSource.this.getClass().getName();
83            log(TAG, "type is " + type);
84            return type;
85        }
86    }
87
88    private final LinkedList<ImageData> mImageQueue;
89    private final int mMaxQueueSize;
90    private final float mMaxCropRatio;
91    private final int mBadImageSkipLimit;
92    private final PhotoSource mFallbackSource;
93    private final HashMap<Bitmap, ImageData> mImageMap;
94
95    protected final Context mContext;
96    protected final Resources mResources;
97    protected final Random mRNG;
98    protected final AlbumSettings mSettings;
99    protected final ContentResolver mResolver;
100
101    protected String mSourceName;
102
103    public PhotoSource(Context context, SharedPreferences settings) {
104        this(context, settings, new StockSource(context, settings));
105    }
106
107    public PhotoSource(Context context, SharedPreferences settings, PhotoSource fallbackSource) {
108        mSourceName = TAG;
109        mContext = context;
110        mSettings = AlbumSettings.getAlbumSettings(settings);
111        mResolver = mContext.getContentResolver();
112        mResources = context.getResources();
113        mImageQueue = new LinkedList<ImageData>();
114        mMaxQueueSize = mResources.getInteger(R.integer.image_queue_size);
115        mMaxCropRatio = mResources.getInteger(R.integer.max_crop_ratio) / 1000000f;
116        mBadImageSkipLimit = mResources.getInteger(R.integer.bad_image_skip_limit);
117        mImageMap = new HashMap<Bitmap, ImageData>();
118        mRNG = new Random();
119        mFallbackSource = fallbackSource;
120    }
121
122    protected void fillQueue() {
123        log(TAG, "filling queue");
124        mImageQueue.addAll(findImages(mMaxQueueSize - mImageQueue.size()));
125        Collections.shuffle(mImageQueue);
126        log(TAG, "queue contains: " + mImageQueue.size() + " items.");
127    }
128
129    public Bitmap next(BitmapFactory.Options options, int longSide, int shortSide) {
130        log(TAG, "decoding a picasa resource to " +  longSide + ", " + shortSide);
131        Bitmap image = null;
132        ImageData imageData = null;
133        int tries = 0;
134
135        while (image == null && tries < mBadImageSkipLimit) {
136            synchronized(mImageQueue) {
137                if (mImageQueue.isEmpty()) {
138                    fillQueue();
139                }
140                imageData = mImageQueue.poll();
141            }
142            if (imageData != null) {
143                image = load(imageData, options, longSide, shortSide);
144                mImageMap.put(image, imageData);
145                imageData = null;
146            }
147
148            tries++;
149        }
150
151        if (image == null && mFallbackSource != null) {
152            image = load((ImageData) mFallbackSource.findImages(1).toArray()[0],
153                    options, longSide, shortSide);
154        }
155
156        return image;
157    }
158
159    public Bitmap load(ImageData data, BitmapFactory.Options options, int longSide, int shortSide) {
160        log(TAG, "decoding photo resource to " +  longSide + ", " + shortSide);
161        InputStream is = data.getStream(longSide);
162
163        Bitmap image = null;
164        try {
165            BufferedInputStream bis = new BufferedInputStream(is);
166            bis.mark(BUFFER_SIZE);
167
168            options.inJustDecodeBounds = true;
169            options.inSampleSize = 1;
170            image = BitmapFactory.decodeStream(new BufferedInputStream(bis), null, options);
171            int rawLongSide = Math.max(options.outWidth, options.outHeight);
172            int rawShortSide = Math.min(options.outWidth, options.outHeight);
173            log(TAG, "I see bounds of " +  rawLongSide + ", " + rawShortSide);
174
175            if (rawLongSide != -1 && rawShortSide != -1) {
176                float insideRatio = Math.max((float) longSide / (float) rawLongSide,
177                                             (float) shortSide / (float) rawShortSide);
178                float outsideRatio = Math.max((float) longSide / (float) rawLongSide,
179                                              (float) shortSide / (float) rawShortSide);
180                float ratio = (outsideRatio / insideRatio < mMaxCropRatio ?
181                               outsideRatio : insideRatio);
182
183                while (ratio < 0.5) {
184                    options.inSampleSize *= 2;
185                    ratio *= 2;
186                }
187
188                log(TAG, "decoding with inSampleSize " +  options.inSampleSize);
189                try {
190                    bis.reset();
191                } catch (IOException ioe) {
192                    // start over, something went wrong and we read too far into the image.
193                    bis.close();
194                    is = data.getStream(longSide);
195                    bis = new BufferedInputStream(is);
196                    log(TAG, "resetting the stream");
197                }
198                options.inJustDecodeBounds = false;
199                image = BitmapFactory.decodeStream(bis, null, options);
200                rawLongSide = Math.max(options.outWidth, options.outHeight);
201                rawShortSide = Math.max(options.outWidth, options.outHeight);
202                if (image != null && rawLongSide != -1 && rawShortSide != -1) {
203                    ratio = Math.max((float) longSide / (float) rawLongSide,
204                            (float) shortSide / (float) rawShortSide);
205
206                    if (Math.abs(ratio - 1.0f) > 0.001) {
207                        log(TAG, "still too big, scaling down by " + ratio);
208                        options.outWidth = (int) (ratio * options.outWidth);
209                        options.outHeight = (int) (ratio * options.outHeight);
210
211                        image = Bitmap.createScaledBitmap(image,
212                                options.outWidth, options.outHeight,
213                                true);
214                    }
215
216                    if (data.orientation != 0) {
217                        log(TAG, "rotated by " + data.orientation + ": fixing");
218                        Matrix matrix = new Matrix();
219                        matrix.setRotate(data.orientation,
220                                (float) Math.floor(image.getWidth() / 2f),
221                                (float) Math.floor(image.getHeight() / 2f));
222                        image = Bitmap.createBitmap(image, 0, 0,
223                                                    options.outWidth, options.outHeight,
224                                                    matrix, true);
225                        if (data.orientation == 90 || data.orientation == 270) {
226                            int tmp = options.outWidth;
227                            options.outWidth = options.outHeight;
228                            options.outHeight = tmp;
229                        }
230                    }
231
232                    log(TAG, "returning bitmap " + image.getWidth() + ", " + image.getHeight());
233                } else {
234                    image = null;
235                }
236            } else {
237                image = null;
238            }
239            if (image == null) {
240                log(TAG, "Stream decoding failed with no error" +
241                        (options.mCancel ? " due to cancelation." : "."));
242            }
243        } catch (OutOfMemoryError ome) {
244            log(TAG, "OUT OF MEMORY: " + ome);
245            image = null;
246        } catch (FileNotFoundException fnf) {
247            log(TAG, "file not found: " + fnf);
248            image = null;
249        } catch (IOException ioe) {
250            log(TAG, "i/o exception: " + ioe);
251            image = null;
252        } finally {
253            try {
254                if (is != null) {
255                    is.close();
256                }
257            } catch (Throwable t) {
258                log(TAG, "close fail: " + t.toString());
259            }
260        }
261
262        return image;
263    }
264
265    public void setSeed(long seed) {
266        mRNG.setSeed(seed);
267    }
268
269    protected static void log(String tag, String message) {
270        if (DEBUG) {
271            Log.i(tag, message);
272        }
273    }
274
275    protected int pickRandomStart(int total, int max) {
276        if (max >= total) {
277            return -1;
278        } else {
279            return (mRNG.nextInt() % (total - max)) - 1;
280        }
281    }
282
283    public Bitmap naturalNext(Bitmap current, BitmapFactory.Options options,
284            int longSide, int shortSide) {
285        Bitmap image = null;
286        ImageData data = mImageMap.get(current);
287        if (data != null) {
288          ImageData next = data.naturalNext();
289          if (next != null) {
290            image = load(next, options, longSide, shortSide);
291            mImageMap.put(image, next);
292          }
293        }
294        return image;
295    }
296
297    public Bitmap naturalPrevious(Bitmap current, BitmapFactory.Options options,
298            int longSide, int shortSide) {
299        Bitmap image = null;
300        ImageData data = mImageMap.get(current);
301        if (current != null) {
302          ImageData prev = data.naturalPrevious();
303          if (prev != null) {
304            image = load(prev, options, longSide, shortSide);
305            mImageMap.put(image, prev);
306          }
307        }
308        return image;
309    }
310
311    public void donePaging(Bitmap current) {
312        ImageData data = mImageMap.get(current);
313        if (data != null) {
314            data.donePaging();
315        }
316    }
317
318    public void recycle(Bitmap trash) {
319        if (trash != null) {
320            mImageMap.remove(trash);
321            trash.recycle();
322        }
323    }
324
325    protected abstract InputStream getStream(ImageData data, int longSide);
326    protected abstract Collection<ImageData> findImages(int howMany);
327    protected abstract ImageData naturalNext(ImageData current);
328    protected abstract ImageData naturalPrevious(ImageData current);
329    protected abstract void donePaging(ImageData current);
330
331    public abstract Collection<AlbumData> findAlbums();
332}
333