• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ContentResolver;
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.content.res.Resources;
22 import android.database.Cursor;
23 import android.graphics.Bitmap;
24 import android.graphics.BitmapFactory;
25 import android.graphics.Matrix;
26 import android.util.Log;
27 
28 import java.io.BufferedInputStream;
29 import java.io.FileNotFoundException;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.LinkedList;
36 import java.util.Random;
37 
38 /**
39  * Picks a random image from a source of photos.
40  */
41 public 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 
getStream(int longSide)58         InputStream getStream(int longSide) {
59             return PhotoSource.this.getStream(this, longSide);
60         }
naturalNext()61         ImageData naturalNext() {
62             return PhotoSource.this.naturalNext(this);
63         }
naturalPrevious()64         ImageData naturalPrevious() {
65             return PhotoSource.this.naturalPrevious(this);
66         }
donePaging()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 
getType()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 
PhotoSource(Context context, SharedPreferences settings)101     public PhotoSource(Context context, SharedPreferences settings) {
102         this(context, settings, new StockSource(context, settings));
103     }
104 
PhotoSource(Context context, SharedPreferences settings, PhotoSource fallbackSource)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 
fillQueue()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 
next(BitmapFactory.Options options, int longSide, int shortSide)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 
load(ImageData data, BitmapFactory.Options options, int longSide, int shortSide)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 
setSeed(long seed)255     public void setSeed(long seed) {
256         mRNG.setSeed(seed);
257     }
258 
log(String tag, String message)259     protected static void log(String tag, String message) {
260         if (DEBUG) {
261             Log.i(tag, message);
262         }
263     }
264 
pickRandomStart(int total, int max)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 
naturalNext(Bitmap current, BitmapFactory.Options options, int longSide, int shortSide)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 
naturalPrevious(Bitmap current, BitmapFactory.Options options, int longSide, int shortSide)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 
donePaging(Bitmap current)301     public void donePaging(Bitmap current) {
302         ImageData data = mImageMap.get(current);
303         if (data != null) {
304             data.donePaging();
305         }
306     }
307 
recycle(Bitmap trash)308     public void recycle(Bitmap trash) {
309         if (trash != null) {
310             mImageMap.remove(trash);
311             trash.recycle();
312         }
313     }
314 
getStream(ImageData data, int longSide)315     protected abstract InputStream getStream(ImageData data, int longSide);
findImages(int howMany)316     protected abstract Collection<ImageData> findImages(int howMany);
naturalNext(ImageData current)317     protected abstract ImageData naturalNext(ImageData current);
naturalPrevious(ImageData current)318     protected abstract ImageData naturalPrevious(ImageData current);
donePaging(ImageData current)319     protected abstract void donePaging(ImageData current);
320 
findAlbums()321     public abstract Collection<AlbumData> findAlbums();
322 }
323