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 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