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.net.Uri;
27 import android.util.Log;
28 
29 import java.io.BufferedInputStream;
30 import java.io.FileNotFoundException;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.HashMap;
36 import java.util.LinkedList;
37 import java.util.Random;
38 
39 /**
40  * Picks a random image from a source of photos.
41  */
42 public 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 
getStream(int longSide)60         InputStream getStream(int longSide) {
61             return PhotoSource.this.getStream(this, longSide);
62         }
naturalNext()63         ImageData naturalNext() {
64             return PhotoSource.this.naturalNext(this);
65         }
naturalPrevious()66         ImageData naturalPrevious() {
67             return PhotoSource.this.naturalPrevious(this);
68         }
donePaging()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 
getType()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 
PhotoSource(Context context, SharedPreferences settings)103     public PhotoSource(Context context, SharedPreferences settings) {
104         this(context, settings, new StockSource(context, settings));
105     }
106 
PhotoSource(Context context, SharedPreferences settings, PhotoSource fallbackSource)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 
fillQueue()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 
next(BitmapFactory.Options options, int longSide, int shortSide)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 
load(ImageData data, BitmapFactory.Options options, int longSide, int shortSide)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 
setSeed(long seed)265     public void setSeed(long seed) {
266         mRNG.setSeed(seed);
267     }
268 
log(String tag, String message)269     protected static void log(String tag, String message) {
270         if (DEBUG) {
271             Log.i(tag, message);
272         }
273     }
274 
pickRandomStart(int total, int max)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 
naturalNext(Bitmap current, BitmapFactory.Options options, int longSide, int shortSide)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 
naturalPrevious(Bitmap current, BitmapFactory.Options options, int longSide, int shortSide)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 
donePaging(Bitmap current)311     public void donePaging(Bitmap current) {
312         ImageData data = mImageMap.get(current);
313         if (data != null) {
314             data.donePaging();
315         }
316     }
317 
recycle(Bitmap trash)318     public void recycle(Bitmap trash) {
319         if (trash != null) {
320             mImageMap.remove(trash);
321             trash.recycle();
322         }
323     }
324 
getStream(ImageData data, int longSide)325     protected abstract InputStream getStream(ImageData data, int longSide);
findImages(int howMany)326     protected abstract Collection<ImageData> findImages(int howMany);
naturalNext(ImageData current)327     protected abstract ImageData naturalNext(ImageData current);
naturalPrevious(ImageData current)328     protected abstract ImageData naturalPrevious(ImageData current);
donePaging(ImageData current)329     protected abstract void donePaging(ImageData current);
330 
findAlbums()331     public abstract Collection<AlbumData> findAlbums();
332 }
333