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.provider.MediaStore; 28 import android.util.Log; 29 30 import java.io.FileNotFoundException; 31 import java.io.InputStream; 32 import java.io.IOException; 33 import java.io.BufferedInputStream; 34 import java.util.Collection; 35 import java.util.Collections; 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 = 128 * 1024; 49 50 public class ImageData { 51 public String id; 52 public String url; 53 public int orientation; 54 getStream(int longSide)55 InputStream getStream(int longSide) { 56 return PhotoSource.this.getStream(this, longSide); 57 } 58 } 59 60 public class AlbumData { 61 public String id; 62 public String title; 63 public String thumbnailUrl; 64 public String account; 65 public long updated; 66 getType()67 public String getType() { 68 String type = PhotoSource.this.getClass().getName(); 69 log(TAG, "type is " + type); 70 return type; 71 } 72 } 73 74 private final LinkedList<ImageData> mImageQueue; 75 private final int mMaxQueueSize; 76 private final float mMaxCropRatio; 77 private final int mBadImageSkipLimit; 78 private final PhotoSource mFallbackSource; 79 80 protected final Context mContext; 81 protected final Resources mResources; 82 protected final Random mRNG; 83 protected final AlbumSettings mSettings; 84 protected final ContentResolver mResolver; 85 86 protected String mSourceName; 87 PhotoSource(Context context, SharedPreferences settings)88 public PhotoSource(Context context, SharedPreferences settings) { 89 this(context, settings, new StockSource(context, settings)); 90 } 91 PhotoSource(Context context, SharedPreferences settings, PhotoSource fallbackSource)92 public PhotoSource(Context context, SharedPreferences settings, PhotoSource fallbackSource) { 93 mSourceName = TAG; 94 mContext = context; 95 mSettings = AlbumSettings.getAlbumSettings(settings); 96 mResolver = mContext.getContentResolver(); 97 mResources = context.getResources(); 98 mImageQueue = new LinkedList<ImageData>(); 99 mMaxQueueSize = mResources.getInteger(R.integer.image_queue_size); 100 mMaxCropRatio = mResources.getInteger(R.integer.max_crop_ratio) / 1000000f; 101 mBadImageSkipLimit = mResources.getInteger(R.integer.bad_image_skip_limit); 102 mRNG = new Random(); 103 mFallbackSource = fallbackSource; 104 } 105 fillQueue()106 protected void fillQueue() { 107 log(TAG, "filling queue"); 108 mImageQueue.addAll(findImages(mMaxQueueSize - mImageQueue.size())); 109 Collections.shuffle(mImageQueue); 110 log(TAG, "queue contains: " + mImageQueue.size() + " items."); 111 } 112 next(BitmapFactory.Options options, int longSide, int shortSide)113 public Bitmap next(BitmapFactory.Options options, int longSide, int shortSide) { 114 log(TAG, "decoding a picasa resource to " + longSide + ", " + shortSide); 115 Bitmap image = null; 116 ImageData imageData = null; 117 int tries = 0; 118 119 while (image == null && tries < mBadImageSkipLimit) { 120 synchronized(mImageQueue) { 121 if (mImageQueue.isEmpty()) { 122 fillQueue(); 123 } 124 125 imageData = mImageQueue.poll(); 126 } 127 if (imageData != null) { 128 image = load(imageData, options, longSide, shortSide); 129 imageData = null; 130 } 131 132 tries++; 133 } 134 135 if (image == null && mFallbackSource != null) { 136 image = load((ImageData) mFallbackSource.findImages(1).toArray()[0], 137 options, longSide, shortSide); 138 } 139 140 return image; 141 } 142 load(ImageData data, BitmapFactory.Options options, int longSide, int shortSide)143 public Bitmap load(ImageData data, BitmapFactory.Options options, int longSide, int shortSide) { 144 log(TAG, "decoding photo resource to " + longSide + ", " + shortSide); 145 InputStream is = data.getStream(longSide); 146 147 Bitmap image = null; 148 try { 149 BufferedInputStream bis = new BufferedInputStream(is); 150 bis.mark(BUFFER_SIZE); 151 152 options.inJustDecodeBounds = true; 153 options.inSampleSize = 1; 154 image = BitmapFactory.decodeStream(new BufferedInputStream(bis), null, options); 155 int rawLongSide = Math.max(options.outWidth, options.outHeight); 156 int rawShortSide = Math.min(options.outWidth, options.outHeight); 157 log(TAG, "I see bounds of " + rawLongSide + ", " + rawShortSide); 158 159 if (rawLongSide != -1 && rawShortSide != -1) { 160 float insideRatio = Math.max((float) longSide / (float) rawLongSide, 161 (float) shortSide / (float) rawShortSide); 162 float outsideRatio = Math.max((float) longSide / (float) rawLongSide, 163 (float) shortSide / (float) rawShortSide); 164 float ratio = (outsideRatio / insideRatio < mMaxCropRatio ? 165 outsideRatio : insideRatio); 166 167 while (ratio < 0.5) { 168 options.inSampleSize *= 2; 169 ratio *= 2; 170 } 171 172 log(TAG, "decoding with inSampleSize " + options.inSampleSize); 173 bis.reset(); 174 options.inJustDecodeBounds = false; 175 image = BitmapFactory.decodeStream(bis, null, options); 176 rawLongSide = Math.max(options.outWidth, options.outHeight); 177 rawShortSide = Math.max(options.outWidth, options.outHeight); 178 if (image != null && rawLongSide != -1 && rawShortSide != -1) { 179 ratio = Math.max((float) longSide / (float) rawLongSide, 180 (float) shortSide / (float) rawShortSide); 181 182 if (Math.abs(ratio - 1.0f) > 0.001) { 183 log(TAG, "still too big, scaling down by " + ratio); 184 options.outWidth = (int) (ratio * options.outWidth); 185 options.outHeight = (int) (ratio * options.outHeight); 186 187 image = Bitmap.createScaledBitmap(image, 188 options.outWidth, options.outHeight, 189 true); 190 } 191 192 if (data.orientation != 0) { 193 log(TAG, "rotated by " + data.orientation + ": fixing"); 194 Matrix matrix = new Matrix(); 195 matrix.setRotate(data.orientation, 196 (float) Math.floor(image.getWidth() / 2f), 197 (float) Math.floor(image.getHeight() / 2f)); 198 image = Bitmap.createBitmap(image, 0, 0, 199 options.outWidth, options.outHeight, 200 matrix, true); 201 if (data.orientation == 90 || data.orientation == 270) { 202 int tmp = options.outWidth; 203 options.outWidth = options.outHeight; 204 options.outHeight = tmp; 205 } 206 } 207 208 log(TAG, "returning bitmap " + image.getWidth() + ", " + image.getHeight()); 209 } else { 210 image = null; 211 } 212 } else { 213 image = null; 214 } 215 if (image == null) { 216 log(TAG, "Stream decoding failed with no error" + 217 (options.mCancel ? " due to cancelation." : ".")); 218 } 219 } catch (OutOfMemoryError ome) { 220 log(TAG, "OUT OF MEMORY: " + ome); 221 image = null; 222 } catch (FileNotFoundException fnf) { 223 log(TAG, "file not found: " + fnf); 224 image = null; 225 } catch (IOException ioe) { 226 log(TAG, "i/o exception: " + ioe); 227 image = null; 228 } finally { 229 try { 230 if (is != null) { 231 is.close(); 232 } 233 } catch (Throwable t) { 234 log(TAG, "close fail: " + t.toString()); 235 } 236 } 237 238 return image; 239 } 240 setSeed(long seed)241 public void setSeed(long seed) { 242 mRNG.setSeed(seed); 243 } 244 log(String tag, String message)245 protected static void log(String tag, String message) { 246 if (DEBUG) { 247 Log.i(tag, message); 248 } 249 } 250 getStream(ImageData data, int longSide)251 protected abstract InputStream getStream(ImageData data, int longSide); findImages(int howMany)252 protected abstract Collection<ImageData> findImages(int howMany); findAlbums()253 public abstract Collection<AlbumData> findAlbums(); 254 } 255