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