/* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.net.NetworkInfo; import android.os.Build; import android.view.Gravity; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import okio.BufferedSource; import okio.Okio; import okio.Source; import static android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL; import static android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL; import static android.media.ExifInterface.ORIENTATION_ROTATE_180; import static android.media.ExifInterface.ORIENTATION_ROTATE_270; import static android.media.ExifInterface.ORIENTATION_ROTATE_90; import static android.media.ExifInterface.ORIENTATION_TRANSPOSE; import static android.media.ExifInterface.ORIENTATION_TRANSVERSE; import static com.squareup.picasso.MemoryPolicy.shouldReadFromMemoryCache; import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY; import static com.squareup.picasso.Picasso.Priority; import static com.squareup.picasso.Picasso.Priority.LOW; import static com.squareup.picasso.Utils.OWNER_HUNTER; import static com.squareup.picasso.Utils.VERB_DECODED; import static com.squareup.picasso.Utils.VERB_EXECUTING; import static com.squareup.picasso.Utils.VERB_JOINED; import static com.squareup.picasso.Utils.VERB_REMOVED; import static com.squareup.picasso.Utils.VERB_TRANSFORMED; import static com.squareup.picasso.Utils.getLogIdsForHunter; import static com.squareup.picasso.Utils.log; class BitmapHunter implements Runnable { /** * Global lock for bitmap decoding to ensure that we are only are decoding one at a time. Since * this will only ever happen in background threads we help avoid excessive memory thrashing as * well as potential OOMs. Shamelessly stolen from Volley. */ private static final Object DECODE_LOCK = new Object(); private static final ThreadLocal<StringBuilder> NAME_BUILDER = new ThreadLocal<StringBuilder>() { @Override protected StringBuilder initialValue() { return new StringBuilder(Utils.THREAD_PREFIX); } }; private static final AtomicInteger SEQUENCE_GENERATOR = new AtomicInteger(); private static final RequestHandler ERRORING_HANDLER = new RequestHandler() { @Override public boolean canHandleRequest(Request data) { return true; } @Override public Result load(Request request, int networkPolicy) throws IOException { throw new IllegalStateException("Unrecognized type of request: " + request); } }; final int sequence; final Picasso picasso; final Dispatcher dispatcher; final Cache cache; final Stats stats; final String key; final Request data; final int memoryPolicy; int networkPolicy; final RequestHandler requestHandler; Action action; List<Action> actions; Bitmap result; Future<?> future; Picasso.LoadedFrom loadedFrom; Exception exception; int exifOrientation; // Determined during decoding of original resource. int retryCount; Priority priority; BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action, RequestHandler requestHandler) { this.sequence = SEQUENCE_GENERATOR.incrementAndGet(); this.picasso = picasso; this.dispatcher = dispatcher; this.cache = cache; this.stats = stats; this.action = action; this.key = action.getKey(); this.data = action.getRequest(); this.priority = action.getPriority(); this.memoryPolicy = action.getMemoryPolicy(); this.networkPolicy = action.getNetworkPolicy(); this.requestHandler = requestHandler; this.retryCount = requestHandler.getRetryCount(); } /** * Decode a byte stream into a Bitmap. This method will take into account additional information * about the supplied request in order to do the decoding efficiently (such as through leveraging * {@code inSampleSize}). */ static Bitmap decodeStream(Source source, Request request) throws IOException { BufferedSource bufferedSource = Okio.buffer(source); boolean isWebPFile = Utils.isWebPFile(bufferedSource); boolean isPurgeable = request.purgeable && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP; BitmapFactory.Options options = RequestHandler.createBitmapOptions(request); boolean calculateSize = RequestHandler.requiresInSampleSize(options); // We decode from a byte array because, a) when decoding a WebP network stream, BitmapFactory // throws a JNI Exception, so we workaround by decoding a byte array, or b) user requested // purgeable, which only affects bitmaps decoded from byte arrays. if (isWebPFile || isPurgeable) { byte[] bytes = bufferedSource.readByteArray(); if (calculateSize) { BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); RequestHandler.calculateInSampleSize(request.targetWidth, request.targetHeight, options, request); } return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); } else { InputStream stream = bufferedSource.inputStream(); if (calculateSize) { // TODO use an InputStream that buffers with Okio... MarkableInputStream markStream = new MarkableInputStream(stream); stream = markStream; markStream.allowMarksToExpire(false); long mark = markStream.savePosition(1024); BitmapFactory.decodeStream(stream, null, options); RequestHandler.calculateInSampleSize(request.targetWidth, request.targetHeight, options, request); markStream.reset(mark); markStream.allowMarksToExpire(true); } Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options); if (bitmap == null) { // Treat null as an IO exception, we will eventually retry. throw new IOException("Failed to decode stream."); } return bitmap; } } @Override public void run() { try { updateThreadName(data); if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this)); } result = hunt(); if (result == null) { dispatcher.dispatchFailed(this); } else { dispatcher.dispatchComplete(this); } } catch (NetworkRequestHandler.ResponseException e) { if (!NetworkPolicy.isOfflineOnly(e.networkPolicy) || e.code != 504) { exception = e; } dispatcher.dispatchFailed(this); } catch (IOException e) { exception = e; dispatcher.dispatchRetry(this); } catch (OutOfMemoryError e) { StringWriter writer = new StringWriter(); stats.createSnapshot().dump(new PrintWriter(writer)); exception = new RuntimeException(writer.toString(), e); dispatcher.dispatchFailed(this); } catch (Exception e) { exception = e; dispatcher.dispatchFailed(this); } finally { Thread.currentThread().setName(Utils.THREAD_IDLE_NAME); } } Bitmap hunt() throws IOException { Bitmap bitmap = null; if (shouldReadFromMemoryCache(memoryPolicy)) { bitmap = cache.get(key); if (bitmap != null) { stats.dispatchCacheHit(); loadedFrom = MEMORY; if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache"); } return bitmap; } } networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy; RequestHandler.Result result = requestHandler.load(data, networkPolicy); if (result != null) { loadedFrom = result.getLoadedFrom(); exifOrientation = result.getExifOrientation(); bitmap = result.getBitmap(); // If there was no Bitmap then we need to decode it from the stream. if (bitmap == null) { Source source = result.getSource(); try { bitmap = decodeStream(source, data); } finally { try { //noinspection ConstantConditions If bitmap is null then source is guranteed non-null. source.close(); } catch (IOException ignored) { } } } } if (bitmap != null) { if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_DECODED, data.logId()); } stats.dispatchBitmapDecoded(bitmap); if (data.needsTransformation() || exifOrientation != 0) { synchronized (DECODE_LOCK) { if (data.needsMatrixTransform() || exifOrientation != 0) { bitmap = transformResult(data, bitmap, exifOrientation); if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId()); } } if (data.hasCustomTransformations()) { bitmap = applyCustomTransformations(data.transformations, bitmap); if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations"); } } } if (bitmap != null) { stats.dispatchBitmapTransformed(bitmap); } } } return bitmap; } void attach(Action action) { boolean loggingEnabled = picasso.loggingEnabled; Request request = action.request; if (this.action == null) { this.action = action; if (loggingEnabled) { if (actions == null || actions.isEmpty()) { log(OWNER_HUNTER, VERB_JOINED, request.logId(), "to empty hunter"); } else { log(OWNER_HUNTER, VERB_JOINED, request.logId(), getLogIdsForHunter(this, "to ")); } } return; } if (actions == null) { actions = new ArrayList<>(3); } actions.add(action); if (loggingEnabled) { log(OWNER_HUNTER, VERB_JOINED, request.logId(), getLogIdsForHunter(this, "to ")); } Priority actionPriority = action.getPriority(); if (actionPriority.ordinal() > priority.ordinal()) { priority = actionPriority; } } void detach(Action action) { boolean detached = false; if (this.action == action) { this.action = null; detached = true; } else if (actions != null) { detached = actions.remove(action); } // The action being detached had the highest priority. Update this // hunter's priority with the remaining actions. if (detached && action.getPriority() == priority) { priority = computeNewPriority(); } if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_REMOVED, action.request.logId(), getLogIdsForHunter(this, "from ")); } } private Priority computeNewPriority() { Priority newPriority = LOW; boolean hasMultiple = actions != null && !actions.isEmpty(); boolean hasAny = action != null || hasMultiple; // Hunter has no requests, low priority. if (!hasAny) { return newPriority; } if (action != null) { newPriority = action.getPriority(); } if (hasMultiple) { //noinspection ForLoopReplaceableByForEach for (int i = 0, n = actions.size(); i < n; i++) { Priority actionPriority = actions.get(i).getPriority(); if (actionPriority.ordinal() > newPriority.ordinal()) { newPriority = actionPriority; } } } return newPriority; } boolean cancel() { return action == null && (actions == null || actions.isEmpty()) && future != null && future.cancel(false); } boolean isCancelled() { return future != null && future.isCancelled(); } boolean shouldRetry(boolean airplaneMode, NetworkInfo info) { boolean hasRetries = retryCount > 0; if (!hasRetries) { return false; } retryCount--; return requestHandler.shouldRetry(airplaneMode, info); } boolean supportsReplay() { return requestHandler.supportsReplay(); } Bitmap getResult() { return result; } String getKey() { return key; } int getMemoryPolicy() { return memoryPolicy; } Request getData() { return data; } Action getAction() { return action; } Picasso getPicasso() { return picasso; } List<Action> getActions() { return actions; } Exception getException() { return exception; } Picasso.LoadedFrom getLoadedFrom() { return loadedFrom; } Priority getPriority() { return priority; } static void updateThreadName(Request data) { String name = data.getName(); StringBuilder builder = NAME_BUILDER.get(); builder.ensureCapacity(Utils.THREAD_PREFIX.length() + name.length()); builder.replace(Utils.THREAD_PREFIX.length(), builder.length(), name); Thread.currentThread().setName(builder.toString()); } static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) { Request request = action.getRequest(); List<RequestHandler> requestHandlers = picasso.getRequestHandlers(); // Index-based loop to avoid allocating an iterator. //noinspection ForLoopReplaceableByForEach for (int i = 0, count = requestHandlers.size(); i < count; i++) { RequestHandler requestHandler = requestHandlers.get(i); if (requestHandler.canHandleRequest(request)) { return new BitmapHunter(picasso, dispatcher, cache, stats, action, requestHandler); } } return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER); } static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) { for (int i = 0, count = transformations.size(); i < count; i++) { final Transformation transformation = transformations.get(i); Bitmap newResult; try { newResult = transformation.transform(result); } catch (final RuntimeException e) { Picasso.HANDLER.post(new Runnable() { @Override public void run() { throw new RuntimeException( "Transformation " + transformation.key() + " crashed with exception.", e); } }); return null; } if (newResult == null) { final StringBuilder builder = new StringBuilder() // .append("Transformation ") .append(transformation.key()) .append(" returned null after ") .append(i) .append(" previous transformation(s).\n\nTransformation list:\n"); for (Transformation t : transformations) { builder.append(t.key()).append('\n'); } Picasso.HANDLER.post(new Runnable() { @Override public void run() { throw new NullPointerException(builder.toString()); } }); return null; } if (newResult == result && result.isRecycled()) { Picasso.HANDLER.post(new Runnable() { @Override public void run() { throw new IllegalStateException("Transformation " + transformation.key() + " returned input Bitmap but recycled it."); } }); return null; } // If the transformation returned a new bitmap ensure they recycled the original. if (newResult != result && !result.isRecycled()) { Picasso.HANDLER.post(new Runnable() { @Override public void run() { throw new IllegalStateException("Transformation " + transformation.key() + " mutated input Bitmap but failed to recycle the original."); } }); return null; } result = newResult; } return result; } static Bitmap transformResult(Request data, Bitmap result, int exifOrientation) { int inWidth = result.getWidth(); int inHeight = result.getHeight(); boolean onlyScaleDown = data.onlyScaleDown; int drawX = 0; int drawY = 0; int drawWidth = inWidth; int drawHeight = inHeight; Matrix matrix = new Matrix(); if (data.needsMatrixTransform() || exifOrientation != 0) { int targetWidth = data.targetWidth; int targetHeight = data.targetHeight; float targetRotation = data.rotationDegrees; if (targetRotation != 0) { double cosR = Math.cos(Math.toRadians(targetRotation)); double sinR = Math.sin(Math.toRadians(targetRotation)); if (data.hasRotationPivot) { matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY); // Recalculate dimensions after rotation around pivot point double x1T = data.rotationPivotX * (1.0 - cosR) + (data.rotationPivotY * sinR); double y1T = data.rotationPivotY * (1.0 - cosR) - (data.rotationPivotX * sinR); double x2T = x1T + (data.targetWidth * cosR); double y2T = y1T + (data.targetWidth * sinR); double x3T = x1T + (data.targetWidth * cosR) - (data.targetHeight * sinR); double y3T = y1T + (data.targetWidth * sinR) + (data.targetHeight * cosR); double x4T = x1T - (data.targetHeight * sinR); double y4T = y1T + (data.targetHeight * cosR); double maxX = Math.max(x4T, Math.max(x3T, Math.max(x1T, x2T))); double minX = Math.min(x4T, Math.min(x3T, Math.min(x1T, x2T))); double maxY = Math.max(y4T, Math.max(y3T, Math.max(y1T, y2T))); double minY = Math.min(y4T, Math.min(y3T, Math.min(y1T, y2T))); targetWidth = (int) Math.floor(maxX - minX); targetHeight = (int) Math.floor(maxY - minY); } else { matrix.setRotate(targetRotation); // Recalculate dimensions after rotation (around origin) double x1T = 0.0; double y1T = 0.0; double x2T = (data.targetWidth * cosR); double y2T = (data.targetWidth * sinR); double x3T = (data.targetWidth * cosR) - (data.targetHeight * sinR); double y3T = (data.targetWidth * sinR) + (data.targetHeight * cosR); double x4T = -(data.targetHeight * sinR); double y4T = (data.targetHeight * cosR); double maxX = Math.max(x4T, Math.max(x3T, Math.max(x1T, x2T))); double minX = Math.min(x4T, Math.min(x3T, Math.min(x1T, x2T))); double maxY = Math.max(y4T, Math.max(y3T, Math.max(y1T, y2T))); double minY = Math.min(y4T, Math.min(y3T, Math.min(y1T, y2T))); targetWidth = (int) Math.floor(maxX - minX); targetHeight = (int) Math.floor(maxY - minY); } } // EXIf interpretation should be done before cropping in case the dimensions need to // be recalculated if (exifOrientation != 0) { int exifRotation = getExifRotation(exifOrientation); int exifTranslation = getExifTranslation(exifOrientation); if (exifRotation != 0) { matrix.preRotate(exifRotation); if (exifRotation == 90 || exifRotation == 270) { // Recalculate dimensions after exif rotation int tmpHeight = targetHeight; targetHeight = targetWidth; targetWidth = tmpHeight; } } if (exifTranslation != 1) { matrix.postScale(exifTranslation, 1); } } if (data.centerCrop) { // Keep aspect ratio if one dimension is set to 0 float widthRatio = targetWidth != 0 ? targetWidth / (float) inWidth : targetHeight / (float) inHeight; float heightRatio = targetHeight != 0 ? targetHeight / (float) inHeight : targetWidth / (float) inWidth; float scaleX, scaleY; if (widthRatio > heightRatio) { int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio)); if ((data.centerCropGravity & Gravity.TOP) == Gravity.TOP) { drawY = 0; } else if ((data.centerCropGravity & Gravity.BOTTOM) == Gravity.BOTTOM) { drawY = inHeight - newSize; } else { drawY = (inHeight - newSize) / 2; } drawHeight = newSize; scaleX = widthRatio; scaleY = targetHeight / (float) drawHeight; } else if (widthRatio < heightRatio) { int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio)); if ((data.centerCropGravity & Gravity.LEFT) == Gravity.LEFT) { drawX = 0; } else if ((data.centerCropGravity & Gravity.RIGHT) == Gravity.RIGHT) { drawX = inWidth - newSize; } else { drawX = (inWidth - newSize) / 2; } drawWidth = newSize; scaleX = targetWidth / (float) drawWidth; scaleY = heightRatio; } else { drawX = 0; drawWidth = inWidth; scaleX = scaleY = heightRatio; } if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) { matrix.preScale(scaleX, scaleY); } } else if (data.centerInside) { // Keep aspect ratio if one dimension is set to 0 float widthRatio = targetWidth != 0 ? targetWidth / (float) inWidth : targetHeight / (float) inHeight; float heightRatio = targetHeight != 0 ? targetHeight / (float) inHeight : targetWidth / (float) inWidth; float scale = widthRatio < heightRatio ? widthRatio : heightRatio; if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) { matrix.preScale(scale, scale); } } else if ((targetWidth != 0 || targetHeight != 0) // && (targetWidth != inWidth || targetHeight != inHeight)) { // If an explicit target size has been specified and they do not match the results bounds, // pre-scale the existing matrix appropriately. // Keep aspect ratio if one dimension is set to 0. float sx = targetWidth != 0 ? targetWidth / (float) inWidth : targetHeight / (float) inHeight; float sy = targetHeight != 0 ? targetHeight / (float) inHeight : targetWidth / (float) inWidth; if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) { matrix.preScale(sx, sy); } } } Bitmap newResult = Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true); if (newResult != result) { result.recycle(); result = newResult; } return result; } private static boolean shouldResize(boolean onlyScaleDown, int inWidth, int inHeight, int targetWidth, int targetHeight) { return !onlyScaleDown || (targetWidth != 0 && inWidth > targetWidth) || (targetHeight != 0 && inHeight > targetHeight); } static int getExifRotation(int orientation) { int rotation; switch (orientation) { case ORIENTATION_ROTATE_90: case ORIENTATION_TRANSPOSE: rotation = 90; break; case ORIENTATION_ROTATE_180: case ORIENTATION_FLIP_VERTICAL: rotation = 180; break; case ORIENTATION_ROTATE_270: case ORIENTATION_TRANSVERSE: rotation = 270; break; default: rotation = 0; } return rotation; } static int getExifTranslation(int orientation) { int translation; switch (orientation) { case ORIENTATION_FLIP_HORIZONTAL: case ORIENTATION_FLIP_VERTICAL: case ORIENTATION_TRANSPOSE: case ORIENTATION_TRANSVERSE: translation = -1; break; default: translation = 1; } return translation; } }