michael@0: /* michael@0: * Copyright (C) 2013 Square, Inc. michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: package com.squareup.picasso; michael@0: michael@0: import android.content.Context; michael@0: import android.graphics.Bitmap; michael@0: import android.graphics.BitmapFactory; michael@0: import android.graphics.Matrix; michael@0: import android.net.NetworkInfo; michael@0: import android.net.Uri; michael@0: import android.provider.MediaStore; michael@0: import java.io.IOException; michael@0: import java.io.PrintWriter; michael@0: import java.io.StringWriter; michael@0: import java.util.ArrayList; michael@0: import java.util.List; michael@0: import java.util.concurrent.Future; michael@0: michael@0: import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; michael@0: import static android.content.ContentResolver.SCHEME_CONTENT; michael@0: import static android.content.ContentResolver.SCHEME_FILE; michael@0: import static android.provider.ContactsContract.Contacts; michael@0: import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY; michael@0: michael@0: abstract class BitmapHunter implements Runnable { michael@0: michael@0: /** michael@0: * Global lock for bitmap decoding to ensure that we are only are decoding one at a time. Since michael@0: * this will only ever happen in background threads we help avoid excessive memory thrashing as michael@0: * well as potential OOMs. Shamelessly stolen from Volley. michael@0: */ michael@0: private static final Object DECODE_LOCK = new Object(); michael@0: private static final String ANDROID_ASSET = "android_asset"; michael@0: protected static final int ASSET_PREFIX_LENGTH = michael@0: (SCHEME_FILE + ":///" + ANDROID_ASSET + "/").length(); michael@0: michael@0: final Picasso picasso; michael@0: final Dispatcher dispatcher; michael@0: final Cache cache; michael@0: final Stats stats; michael@0: final String key; michael@0: final Request data; michael@0: final List actions; michael@0: final boolean skipMemoryCache; michael@0: michael@0: Bitmap result; michael@0: Future future; michael@0: Picasso.LoadedFrom loadedFrom; michael@0: Exception exception; michael@0: int exifRotation; // Determined during decoding of original resource. michael@0: michael@0: BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) { michael@0: this.picasso = picasso; michael@0: this.dispatcher = dispatcher; michael@0: this.cache = cache; michael@0: this.stats = stats; michael@0: this.key = action.getKey(); michael@0: this.data = action.getData(); michael@0: this.skipMemoryCache = action.skipCache; michael@0: this.actions = new ArrayList(4); michael@0: attach(action); michael@0: } michael@0: michael@0: protected void setExifRotation(int exifRotation) { michael@0: this.exifRotation = exifRotation; michael@0: } michael@0: michael@0: @Override public void run() { michael@0: try { michael@0: Thread.currentThread().setName(Utils.THREAD_PREFIX + data.getName()); michael@0: michael@0: result = hunt(); michael@0: michael@0: if (result == null) { michael@0: dispatcher.dispatchFailed(this); michael@0: } else { michael@0: dispatcher.dispatchComplete(this); michael@0: } michael@0: } catch (Downloader.ResponseException e) { michael@0: exception = e; michael@0: dispatcher.dispatchFailed(this); michael@0: } catch (IOException e) { michael@0: exception = e; michael@0: dispatcher.dispatchRetry(this); michael@0: } catch (OutOfMemoryError e) { michael@0: StringWriter writer = new StringWriter(); michael@0: stats.createSnapshot().dump(new PrintWriter(writer)); michael@0: exception = new RuntimeException(writer.toString(), e); michael@0: dispatcher.dispatchFailed(this); michael@0: } catch (Exception e) { michael@0: exception = e; michael@0: dispatcher.dispatchFailed(this); michael@0: } finally { michael@0: Thread.currentThread().setName(Utils.THREAD_IDLE_NAME); michael@0: } michael@0: } michael@0: michael@0: abstract Bitmap decode(Request data) throws IOException; michael@0: michael@0: Bitmap hunt() throws IOException { michael@0: Bitmap bitmap; michael@0: michael@0: if (!skipMemoryCache) { michael@0: bitmap = cache.get(key); michael@0: if (bitmap != null) { michael@0: stats.dispatchCacheHit(); michael@0: loadedFrom = MEMORY; michael@0: return bitmap; michael@0: } michael@0: } michael@0: michael@0: bitmap = decode(data); michael@0: michael@0: if (bitmap != null) { michael@0: stats.dispatchBitmapDecoded(bitmap); michael@0: if (data.needsTransformation() || exifRotation != 0) { michael@0: synchronized (DECODE_LOCK) { michael@0: if (data.needsMatrixTransform() || exifRotation != 0) { michael@0: bitmap = transformResult(data, bitmap, exifRotation); michael@0: } michael@0: if (data.hasCustomTransformations()) { michael@0: bitmap = applyCustomTransformations(data.transformations, bitmap); michael@0: } michael@0: } michael@0: stats.dispatchBitmapTransformed(bitmap); michael@0: } michael@0: } michael@0: michael@0: return bitmap; michael@0: } michael@0: michael@0: void attach(Action action) { michael@0: actions.add(action); michael@0: } michael@0: michael@0: void detach(Action action) { michael@0: actions.remove(action); michael@0: } michael@0: michael@0: boolean cancel() { michael@0: return actions.isEmpty() && future != null && future.cancel(false); michael@0: } michael@0: michael@0: boolean isCancelled() { michael@0: return future != null && future.isCancelled(); michael@0: } michael@0: michael@0: boolean shouldSkipMemoryCache() { michael@0: return skipMemoryCache; michael@0: } michael@0: michael@0: boolean shouldRetry(boolean airplaneMode, NetworkInfo info) { michael@0: return false; michael@0: } michael@0: michael@0: Bitmap getResult() { michael@0: return result; michael@0: } michael@0: michael@0: String getKey() { michael@0: return key; michael@0: } michael@0: michael@0: Request getData() { michael@0: return data; michael@0: } michael@0: michael@0: List getActions() { michael@0: return actions; michael@0: } michael@0: michael@0: Exception getException() { michael@0: return exception; michael@0: } michael@0: michael@0: Picasso.LoadedFrom getLoadedFrom() { michael@0: return loadedFrom; michael@0: } michael@0: michael@0: static BitmapHunter forRequest(Context context, Picasso picasso, Dispatcher dispatcher, michael@0: Cache cache, Stats stats, Action action, Downloader downloader) { michael@0: if (action.getData().resourceId != 0) { michael@0: return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action); michael@0: } michael@0: Uri uri = action.getData().uri; michael@0: String scheme = uri.getScheme(); michael@0: if (SCHEME_CONTENT.equals(scheme)) { michael@0: if (Contacts.CONTENT_URI.getHost().equals(uri.getHost()) // michael@0: && !uri.getPathSegments().contains(Contacts.Photo.CONTENT_DIRECTORY)) { michael@0: return new ContactsPhotoBitmapHunter(context, picasso, dispatcher, cache, stats, action); michael@0: } else if (MediaStore.AUTHORITY.equals(uri.getAuthority())) { michael@0: return new MediaStoreBitmapHunter(context, picasso, dispatcher, cache, stats, action); michael@0: } else { michael@0: return new ContentStreamBitmapHunter(context, picasso, dispatcher, cache, stats, action); michael@0: } michael@0: } else if (SCHEME_FILE.equals(scheme)) { michael@0: if (!uri.getPathSegments().isEmpty() && ANDROID_ASSET.equals(uri.getPathSegments().get(0))) { michael@0: return new AssetBitmapHunter(context, picasso, dispatcher, cache, stats, action); michael@0: } michael@0: return new FileBitmapHunter(context, picasso, dispatcher, cache, stats, action); michael@0: } else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { michael@0: return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action); michael@0: } else { michael@0: return new NetworkBitmapHunter(picasso, dispatcher, cache, stats, action, downloader); michael@0: } michael@0: } michael@0: michael@0: static void calculateInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) { michael@0: calculateInSampleSize(reqWidth, reqHeight, options.outWidth, options.outHeight, options); michael@0: } michael@0: michael@0: static void calculateInSampleSize(int reqWidth, int reqHeight, int width, int height, michael@0: BitmapFactory.Options options) { michael@0: int sampleSize = 1; michael@0: if (height > reqHeight || width > reqWidth) { michael@0: final int heightRatio = Math.round((float) height / (float) reqHeight); michael@0: final int widthRatio = Math.round((float) width / (float) reqWidth); michael@0: sampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; michael@0: } michael@0: michael@0: options.inSampleSize = sampleSize; michael@0: options.inJustDecodeBounds = false; michael@0: } michael@0: michael@0: static Bitmap applyCustomTransformations(List transformations, Bitmap result) { michael@0: for (int i = 0, count = transformations.size(); i < count; i++) { michael@0: final Transformation transformation = transformations.get(i); michael@0: Bitmap newResult = transformation.transform(result); michael@0: michael@0: if (newResult == null) { michael@0: final StringBuilder builder = new StringBuilder() // michael@0: .append("Transformation ") michael@0: .append(transformation.key()) michael@0: .append(" returned null after ") michael@0: .append(i) michael@0: .append(" previous transformation(s).\n\nTransformation list:\n"); michael@0: for (Transformation t : transformations) { michael@0: builder.append(t.key()).append('\n'); michael@0: } michael@0: Picasso.HANDLER.post(new Runnable() { michael@0: @Override public void run() { michael@0: throw new NullPointerException(builder.toString()); michael@0: } michael@0: }); michael@0: return null; michael@0: } michael@0: michael@0: if (newResult == result && result.isRecycled()) { michael@0: Picasso.HANDLER.post(new Runnable() { michael@0: @Override public void run() { michael@0: throw new IllegalStateException("Transformation " michael@0: + transformation.key() michael@0: + " returned input Bitmap but recycled it."); michael@0: } michael@0: }); michael@0: return null; michael@0: } michael@0: michael@0: // If the transformation returned a new bitmap ensure they recycled the original. michael@0: if (newResult != result && !result.isRecycled()) { michael@0: Picasso.HANDLER.post(new Runnable() { michael@0: @Override public void run() { michael@0: throw new IllegalStateException("Transformation " michael@0: + transformation.key() michael@0: + " mutated input Bitmap but failed to recycle the original."); michael@0: } michael@0: }); michael@0: return null; michael@0: } michael@0: michael@0: result = newResult; michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: static Bitmap transformResult(Request data, Bitmap result, int exifRotation) { michael@0: int inWidth = result.getWidth(); michael@0: int inHeight = result.getHeight(); michael@0: michael@0: int drawX = 0; michael@0: int drawY = 0; michael@0: int drawWidth = inWidth; michael@0: int drawHeight = inHeight; michael@0: michael@0: Matrix matrix = new Matrix(); michael@0: michael@0: if (data.needsMatrixTransform()) { michael@0: int targetWidth = data.targetWidth; michael@0: int targetHeight = data.targetHeight; michael@0: michael@0: float targetRotation = data.rotationDegrees; michael@0: if (targetRotation != 0) { michael@0: if (data.hasRotationPivot) { michael@0: matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY); michael@0: } else { michael@0: matrix.setRotate(targetRotation); michael@0: } michael@0: } michael@0: michael@0: if (data.centerCrop) { michael@0: float widthRatio = targetWidth / (float) inWidth; michael@0: float heightRatio = targetHeight / (float) inHeight; michael@0: float scale; michael@0: if (widthRatio > heightRatio) { michael@0: scale = widthRatio; michael@0: int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio)); michael@0: drawY = (inHeight - newSize) / 2; michael@0: drawHeight = newSize; michael@0: } else { michael@0: scale = heightRatio; michael@0: int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio)); michael@0: drawX = (inWidth - newSize) / 2; michael@0: drawWidth = newSize; michael@0: } michael@0: matrix.preScale(scale, scale); michael@0: } else if (data.centerInside) { michael@0: float widthRatio = targetWidth / (float) inWidth; michael@0: float heightRatio = targetHeight / (float) inHeight; michael@0: float scale = widthRatio < heightRatio ? widthRatio : heightRatio; michael@0: matrix.preScale(scale, scale); michael@0: } else if (targetWidth != 0 && targetHeight != 0 // michael@0: && (targetWidth != inWidth || targetHeight != inHeight)) { michael@0: // If an explicit target size has been specified and they do not match the results bounds, michael@0: // pre-scale the existing matrix appropriately. michael@0: float sx = targetWidth / (float) inWidth; michael@0: float sy = targetHeight / (float) inHeight; michael@0: matrix.preScale(sx, sy); michael@0: } michael@0: } michael@0: michael@0: if (exifRotation != 0) { michael@0: matrix.preRotate(exifRotation); michael@0: } michael@0: michael@0: Bitmap newResult = michael@0: Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true); michael@0: if (newResult != result) { michael@0: result.recycle(); michael@0: result = newResult; michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: }