Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* |
michael@0 | 2 | * Copyright (C) 2013 Square, Inc. |
michael@0 | 3 | * |
michael@0 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
michael@0 | 5 | * you may not use this file except in compliance with the License. |
michael@0 | 6 | * You may obtain a copy of the License at |
michael@0 | 7 | * |
michael@0 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
michael@0 | 9 | * |
michael@0 | 10 | * Unless required by applicable law or agreed to in writing, software |
michael@0 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
michael@0 | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
michael@0 | 13 | * See the License for the specific language governing permissions and |
michael@0 | 14 | * limitations under the License. |
michael@0 | 15 | */ |
michael@0 | 16 | package com.squareup.picasso; |
michael@0 | 17 | |
michael@0 | 18 | import android.content.Context; |
michael@0 | 19 | import android.graphics.Bitmap; |
michael@0 | 20 | import android.graphics.BitmapFactory; |
michael@0 | 21 | import android.graphics.Matrix; |
michael@0 | 22 | import android.net.NetworkInfo; |
michael@0 | 23 | import android.net.Uri; |
michael@0 | 24 | import android.provider.MediaStore; |
michael@0 | 25 | import java.io.IOException; |
michael@0 | 26 | import java.io.PrintWriter; |
michael@0 | 27 | import java.io.StringWriter; |
michael@0 | 28 | import java.util.ArrayList; |
michael@0 | 29 | import java.util.List; |
michael@0 | 30 | import java.util.concurrent.Future; |
michael@0 | 31 | |
michael@0 | 32 | import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; |
michael@0 | 33 | import static android.content.ContentResolver.SCHEME_CONTENT; |
michael@0 | 34 | import static android.content.ContentResolver.SCHEME_FILE; |
michael@0 | 35 | import static android.provider.ContactsContract.Contacts; |
michael@0 | 36 | import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY; |
michael@0 | 37 | |
michael@0 | 38 | abstract class BitmapHunter implements Runnable { |
michael@0 | 39 | |
michael@0 | 40 | /** |
michael@0 | 41 | * Global lock for bitmap decoding to ensure that we are only are decoding one at a time. Since |
michael@0 | 42 | * this will only ever happen in background threads we help avoid excessive memory thrashing as |
michael@0 | 43 | * well as potential OOMs. Shamelessly stolen from Volley. |
michael@0 | 44 | */ |
michael@0 | 45 | private static final Object DECODE_LOCK = new Object(); |
michael@0 | 46 | private static final String ANDROID_ASSET = "android_asset"; |
michael@0 | 47 | protected static final int ASSET_PREFIX_LENGTH = |
michael@0 | 48 | (SCHEME_FILE + ":///" + ANDROID_ASSET + "/").length(); |
michael@0 | 49 | |
michael@0 | 50 | final Picasso picasso; |
michael@0 | 51 | final Dispatcher dispatcher; |
michael@0 | 52 | final Cache cache; |
michael@0 | 53 | final Stats stats; |
michael@0 | 54 | final String key; |
michael@0 | 55 | final Request data; |
michael@0 | 56 | final List<Action> actions; |
michael@0 | 57 | final boolean skipMemoryCache; |
michael@0 | 58 | |
michael@0 | 59 | Bitmap result; |
michael@0 | 60 | Future<?> future; |
michael@0 | 61 | Picasso.LoadedFrom loadedFrom; |
michael@0 | 62 | Exception exception; |
michael@0 | 63 | int exifRotation; // Determined during decoding of original resource. |
michael@0 | 64 | |
michael@0 | 65 | BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) { |
michael@0 | 66 | this.picasso = picasso; |
michael@0 | 67 | this.dispatcher = dispatcher; |
michael@0 | 68 | this.cache = cache; |
michael@0 | 69 | this.stats = stats; |
michael@0 | 70 | this.key = action.getKey(); |
michael@0 | 71 | this.data = action.getData(); |
michael@0 | 72 | this.skipMemoryCache = action.skipCache; |
michael@0 | 73 | this.actions = new ArrayList<Action>(4); |
michael@0 | 74 | attach(action); |
michael@0 | 75 | } |
michael@0 | 76 | |
michael@0 | 77 | protected void setExifRotation(int exifRotation) { |
michael@0 | 78 | this.exifRotation = exifRotation; |
michael@0 | 79 | } |
michael@0 | 80 | |
michael@0 | 81 | @Override public void run() { |
michael@0 | 82 | try { |
michael@0 | 83 | Thread.currentThread().setName(Utils.THREAD_PREFIX + data.getName()); |
michael@0 | 84 | |
michael@0 | 85 | result = hunt(); |
michael@0 | 86 | |
michael@0 | 87 | if (result == null) { |
michael@0 | 88 | dispatcher.dispatchFailed(this); |
michael@0 | 89 | } else { |
michael@0 | 90 | dispatcher.dispatchComplete(this); |
michael@0 | 91 | } |
michael@0 | 92 | } catch (Downloader.ResponseException e) { |
michael@0 | 93 | exception = e; |
michael@0 | 94 | dispatcher.dispatchFailed(this); |
michael@0 | 95 | } catch (IOException e) { |
michael@0 | 96 | exception = e; |
michael@0 | 97 | dispatcher.dispatchRetry(this); |
michael@0 | 98 | } catch (OutOfMemoryError e) { |
michael@0 | 99 | StringWriter writer = new StringWriter(); |
michael@0 | 100 | stats.createSnapshot().dump(new PrintWriter(writer)); |
michael@0 | 101 | exception = new RuntimeException(writer.toString(), e); |
michael@0 | 102 | dispatcher.dispatchFailed(this); |
michael@0 | 103 | } catch (Exception e) { |
michael@0 | 104 | exception = e; |
michael@0 | 105 | dispatcher.dispatchFailed(this); |
michael@0 | 106 | } finally { |
michael@0 | 107 | Thread.currentThread().setName(Utils.THREAD_IDLE_NAME); |
michael@0 | 108 | } |
michael@0 | 109 | } |
michael@0 | 110 | |
michael@0 | 111 | abstract Bitmap decode(Request data) throws IOException; |
michael@0 | 112 | |
michael@0 | 113 | Bitmap hunt() throws IOException { |
michael@0 | 114 | Bitmap bitmap; |
michael@0 | 115 | |
michael@0 | 116 | if (!skipMemoryCache) { |
michael@0 | 117 | bitmap = cache.get(key); |
michael@0 | 118 | if (bitmap != null) { |
michael@0 | 119 | stats.dispatchCacheHit(); |
michael@0 | 120 | loadedFrom = MEMORY; |
michael@0 | 121 | return bitmap; |
michael@0 | 122 | } |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | bitmap = decode(data); |
michael@0 | 126 | |
michael@0 | 127 | if (bitmap != null) { |
michael@0 | 128 | stats.dispatchBitmapDecoded(bitmap); |
michael@0 | 129 | if (data.needsTransformation() || exifRotation != 0) { |
michael@0 | 130 | synchronized (DECODE_LOCK) { |
michael@0 | 131 | if (data.needsMatrixTransform() || exifRotation != 0) { |
michael@0 | 132 | bitmap = transformResult(data, bitmap, exifRotation); |
michael@0 | 133 | } |
michael@0 | 134 | if (data.hasCustomTransformations()) { |
michael@0 | 135 | bitmap = applyCustomTransformations(data.transformations, bitmap); |
michael@0 | 136 | } |
michael@0 | 137 | } |
michael@0 | 138 | stats.dispatchBitmapTransformed(bitmap); |
michael@0 | 139 | } |
michael@0 | 140 | } |
michael@0 | 141 | |
michael@0 | 142 | return bitmap; |
michael@0 | 143 | } |
michael@0 | 144 | |
michael@0 | 145 | void attach(Action action) { |
michael@0 | 146 | actions.add(action); |
michael@0 | 147 | } |
michael@0 | 148 | |
michael@0 | 149 | void detach(Action action) { |
michael@0 | 150 | actions.remove(action); |
michael@0 | 151 | } |
michael@0 | 152 | |
michael@0 | 153 | boolean cancel() { |
michael@0 | 154 | return actions.isEmpty() && future != null && future.cancel(false); |
michael@0 | 155 | } |
michael@0 | 156 | |
michael@0 | 157 | boolean isCancelled() { |
michael@0 | 158 | return future != null && future.isCancelled(); |
michael@0 | 159 | } |
michael@0 | 160 | |
michael@0 | 161 | boolean shouldSkipMemoryCache() { |
michael@0 | 162 | return skipMemoryCache; |
michael@0 | 163 | } |
michael@0 | 164 | |
michael@0 | 165 | boolean shouldRetry(boolean airplaneMode, NetworkInfo info) { |
michael@0 | 166 | return false; |
michael@0 | 167 | } |
michael@0 | 168 | |
michael@0 | 169 | Bitmap getResult() { |
michael@0 | 170 | return result; |
michael@0 | 171 | } |
michael@0 | 172 | |
michael@0 | 173 | String getKey() { |
michael@0 | 174 | return key; |
michael@0 | 175 | } |
michael@0 | 176 | |
michael@0 | 177 | Request getData() { |
michael@0 | 178 | return data; |
michael@0 | 179 | } |
michael@0 | 180 | |
michael@0 | 181 | List<Action> getActions() { |
michael@0 | 182 | return actions; |
michael@0 | 183 | } |
michael@0 | 184 | |
michael@0 | 185 | Exception getException() { |
michael@0 | 186 | return exception; |
michael@0 | 187 | } |
michael@0 | 188 | |
michael@0 | 189 | Picasso.LoadedFrom getLoadedFrom() { |
michael@0 | 190 | return loadedFrom; |
michael@0 | 191 | } |
michael@0 | 192 | |
michael@0 | 193 | static BitmapHunter forRequest(Context context, Picasso picasso, Dispatcher dispatcher, |
michael@0 | 194 | Cache cache, Stats stats, Action action, Downloader downloader) { |
michael@0 | 195 | if (action.getData().resourceId != 0) { |
michael@0 | 196 | return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
michael@0 | 197 | } |
michael@0 | 198 | Uri uri = action.getData().uri; |
michael@0 | 199 | String scheme = uri.getScheme(); |
michael@0 | 200 | if (SCHEME_CONTENT.equals(scheme)) { |
michael@0 | 201 | if (Contacts.CONTENT_URI.getHost().equals(uri.getHost()) // |
michael@0 | 202 | && !uri.getPathSegments().contains(Contacts.Photo.CONTENT_DIRECTORY)) { |
michael@0 | 203 | return new ContactsPhotoBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
michael@0 | 204 | } else if (MediaStore.AUTHORITY.equals(uri.getAuthority())) { |
michael@0 | 205 | return new MediaStoreBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
michael@0 | 206 | } else { |
michael@0 | 207 | return new ContentStreamBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
michael@0 | 208 | } |
michael@0 | 209 | } else if (SCHEME_FILE.equals(scheme)) { |
michael@0 | 210 | if (!uri.getPathSegments().isEmpty() && ANDROID_ASSET.equals(uri.getPathSegments().get(0))) { |
michael@0 | 211 | return new AssetBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
michael@0 | 212 | } |
michael@0 | 213 | return new FileBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
michael@0 | 214 | } else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { |
michael@0 | 215 | return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
michael@0 | 216 | } else { |
michael@0 | 217 | return new NetworkBitmapHunter(picasso, dispatcher, cache, stats, action, downloader); |
michael@0 | 218 | } |
michael@0 | 219 | } |
michael@0 | 220 | |
michael@0 | 221 | static void calculateInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) { |
michael@0 | 222 | calculateInSampleSize(reqWidth, reqHeight, options.outWidth, options.outHeight, options); |
michael@0 | 223 | } |
michael@0 | 224 | |
michael@0 | 225 | static void calculateInSampleSize(int reqWidth, int reqHeight, int width, int height, |
michael@0 | 226 | BitmapFactory.Options options) { |
michael@0 | 227 | int sampleSize = 1; |
michael@0 | 228 | if (height > reqHeight || width > reqWidth) { |
michael@0 | 229 | final int heightRatio = Math.round((float) height / (float) reqHeight); |
michael@0 | 230 | final int widthRatio = Math.round((float) width / (float) reqWidth); |
michael@0 | 231 | sampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; |
michael@0 | 232 | } |
michael@0 | 233 | |
michael@0 | 234 | options.inSampleSize = sampleSize; |
michael@0 | 235 | options.inJustDecodeBounds = false; |
michael@0 | 236 | } |
michael@0 | 237 | |
michael@0 | 238 | static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) { |
michael@0 | 239 | for (int i = 0, count = transformations.size(); i < count; i++) { |
michael@0 | 240 | final Transformation transformation = transformations.get(i); |
michael@0 | 241 | Bitmap newResult = transformation.transform(result); |
michael@0 | 242 | |
michael@0 | 243 | if (newResult == null) { |
michael@0 | 244 | final StringBuilder builder = new StringBuilder() // |
michael@0 | 245 | .append("Transformation ") |
michael@0 | 246 | .append(transformation.key()) |
michael@0 | 247 | .append(" returned null after ") |
michael@0 | 248 | .append(i) |
michael@0 | 249 | .append(" previous transformation(s).\n\nTransformation list:\n"); |
michael@0 | 250 | for (Transformation t : transformations) { |
michael@0 | 251 | builder.append(t.key()).append('\n'); |
michael@0 | 252 | } |
michael@0 | 253 | Picasso.HANDLER.post(new Runnable() { |
michael@0 | 254 | @Override public void run() { |
michael@0 | 255 | throw new NullPointerException(builder.toString()); |
michael@0 | 256 | } |
michael@0 | 257 | }); |
michael@0 | 258 | return null; |
michael@0 | 259 | } |
michael@0 | 260 | |
michael@0 | 261 | if (newResult == result && result.isRecycled()) { |
michael@0 | 262 | Picasso.HANDLER.post(new Runnable() { |
michael@0 | 263 | @Override public void run() { |
michael@0 | 264 | throw new IllegalStateException("Transformation " |
michael@0 | 265 | + transformation.key() |
michael@0 | 266 | + " returned input Bitmap but recycled it."); |
michael@0 | 267 | } |
michael@0 | 268 | }); |
michael@0 | 269 | return null; |
michael@0 | 270 | } |
michael@0 | 271 | |
michael@0 | 272 | // If the transformation returned a new bitmap ensure they recycled the original. |
michael@0 | 273 | if (newResult != result && !result.isRecycled()) { |
michael@0 | 274 | Picasso.HANDLER.post(new Runnable() { |
michael@0 | 275 | @Override public void run() { |
michael@0 | 276 | throw new IllegalStateException("Transformation " |
michael@0 | 277 | + transformation.key() |
michael@0 | 278 | + " mutated input Bitmap but failed to recycle the original."); |
michael@0 | 279 | } |
michael@0 | 280 | }); |
michael@0 | 281 | return null; |
michael@0 | 282 | } |
michael@0 | 283 | |
michael@0 | 284 | result = newResult; |
michael@0 | 285 | } |
michael@0 | 286 | return result; |
michael@0 | 287 | } |
michael@0 | 288 | |
michael@0 | 289 | static Bitmap transformResult(Request data, Bitmap result, int exifRotation) { |
michael@0 | 290 | int inWidth = result.getWidth(); |
michael@0 | 291 | int inHeight = result.getHeight(); |
michael@0 | 292 | |
michael@0 | 293 | int drawX = 0; |
michael@0 | 294 | int drawY = 0; |
michael@0 | 295 | int drawWidth = inWidth; |
michael@0 | 296 | int drawHeight = inHeight; |
michael@0 | 297 | |
michael@0 | 298 | Matrix matrix = new Matrix(); |
michael@0 | 299 | |
michael@0 | 300 | if (data.needsMatrixTransform()) { |
michael@0 | 301 | int targetWidth = data.targetWidth; |
michael@0 | 302 | int targetHeight = data.targetHeight; |
michael@0 | 303 | |
michael@0 | 304 | float targetRotation = data.rotationDegrees; |
michael@0 | 305 | if (targetRotation != 0) { |
michael@0 | 306 | if (data.hasRotationPivot) { |
michael@0 | 307 | matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY); |
michael@0 | 308 | } else { |
michael@0 | 309 | matrix.setRotate(targetRotation); |
michael@0 | 310 | } |
michael@0 | 311 | } |
michael@0 | 312 | |
michael@0 | 313 | if (data.centerCrop) { |
michael@0 | 314 | float widthRatio = targetWidth / (float) inWidth; |
michael@0 | 315 | float heightRatio = targetHeight / (float) inHeight; |
michael@0 | 316 | float scale; |
michael@0 | 317 | if (widthRatio > heightRatio) { |
michael@0 | 318 | scale = widthRatio; |
michael@0 | 319 | int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio)); |
michael@0 | 320 | drawY = (inHeight - newSize) / 2; |
michael@0 | 321 | drawHeight = newSize; |
michael@0 | 322 | } else { |
michael@0 | 323 | scale = heightRatio; |
michael@0 | 324 | int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio)); |
michael@0 | 325 | drawX = (inWidth - newSize) / 2; |
michael@0 | 326 | drawWidth = newSize; |
michael@0 | 327 | } |
michael@0 | 328 | matrix.preScale(scale, scale); |
michael@0 | 329 | } else if (data.centerInside) { |
michael@0 | 330 | float widthRatio = targetWidth / (float) inWidth; |
michael@0 | 331 | float heightRatio = targetHeight / (float) inHeight; |
michael@0 | 332 | float scale = widthRatio < heightRatio ? widthRatio : heightRatio; |
michael@0 | 333 | matrix.preScale(scale, scale); |
michael@0 | 334 | } else if (targetWidth != 0 && targetHeight != 0 // |
michael@0 | 335 | && (targetWidth != inWidth || targetHeight != inHeight)) { |
michael@0 | 336 | // If an explicit target size has been specified and they do not match the results bounds, |
michael@0 | 337 | // pre-scale the existing matrix appropriately. |
michael@0 | 338 | float sx = targetWidth / (float) inWidth; |
michael@0 | 339 | float sy = targetHeight / (float) inHeight; |
michael@0 | 340 | matrix.preScale(sx, sy); |
michael@0 | 341 | } |
michael@0 | 342 | } |
michael@0 | 343 | |
michael@0 | 344 | if (exifRotation != 0) { |
michael@0 | 345 | matrix.preRotate(exifRotation); |
michael@0 | 346 | } |
michael@0 | 347 | |
michael@0 | 348 | Bitmap newResult = |
michael@0 | 349 | Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true); |
michael@0 | 350 | if (newResult != result) { |
michael@0 | 351 | result.recycle(); |
michael@0 | 352 | result = newResult; |
michael@0 | 353 | } |
michael@0 | 354 | |
michael@0 | 355 | return result; |
michael@0 | 356 | } |
michael@0 | 357 | } |