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