/*
* 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.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.NetworkInfo;
import android.net.Uri;
import android.provider.MediaStore;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE;
import static android.content.ContentResolver.SCHEME_CONTENT;
import static android.content.ContentResolver.SCHEME_FILE;
import static android.provider.ContactsContract.Contacts;
import static com.squareup.picasso.AssetBitmapHunter.ANDROID_ASSET;
import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
abstract 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);
}
};
final Picasso picasso;
final Dispatcher dispatcher;
final Cache cache;
final Stats stats;
final String key;
final Request data;
@SuppressWarnings("rawtypes")
final List<Action> actions;
final boolean skipMemoryCache;
Bitmap result;
Future<?> future;
Picasso.LoadedFrom loadedFrom;
Exception exception;
int exifRotation; // Determined during decoding of original resource.
@SuppressWarnings("rawtypes")
BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache,
Stats stats, Action action) {
this.picasso = picasso;
this.dispatcher = dispatcher;
this.cache = cache;
this.stats = stats;
this.key = action.getKey();
this.data = action.getData();
this.skipMemoryCache = action.skipCache;
this.actions = new ArrayList<Action>(4);
attach(action);
}
protected void setExifRotation(int exifRotation) {
this.exifRotation = exifRotation;
}
@Override
public void run() {
try {
updateThreadName(data);
result = hunt();
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}
} catch (Downloader.ResponseException e) {
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);
}
}
abstract Bitmap decode(Request data) throws IOException;
Bitmap hunt() throws IOException {
Bitmap bitmap;
if (!skipMemoryCache) {
bitmap = cache.get(key);
if (bitmap != null) {
stats.dispatchCacheHit();
loadedFrom = MEMORY;
return bitmap;
}
}
bitmap = decode(data);
if (bitmap != null) {
stats.dispatchBitmapDecoded(bitmap);
if (data.needsTransformation() || exifRotation != 0) {
synchronized (DECODE_LOCK) {
if (data.needsMatrixTransform() || exifRotation != 0) {
// bitmap = transformResult(data, bitmap, exifRotation);
}
if (data.hasCustomTransformations()) {
bitmap = applyCustomTransformations(
data.transformations, bitmap);
}
}
if (bitmap != null) {
stats.dispatchBitmapTransformed(bitmap);
}
}
}
return bitmap;
}
@SuppressWarnings("rawtypes")
void attach(Action action) {
actions.add(action);
}
@SuppressWarnings("rawtypes")
void detach(Action action) {
actions.remove(action);
}
boolean cancel() {
return actions.isEmpty() && future != null && future.cancel(false);
}
boolean isCancelled() {
return future != null && future.isCancelled();
}
boolean shouldSkipMemoryCache() {
return skipMemoryCache;
}
boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {
return false;
}
Bitmap getResult() {
return result;
}
String getKey() {
return key;
}
Request getData() {
return data;
}
@SuppressWarnings("rawtypes")
List<Action> getActions() {
return actions;
}
Exception getException() {
return exception;
}
Picasso.LoadedFrom getLoadedFrom() {
return loadedFrom;
}
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());
}
@SuppressWarnings("rawtypes")
@SuppressLint("NewApi")
static BitmapHunter forRequest(Context context, Picasso picasso,
Dispatcher dispatcher, Cache cache, Stats stats, Action action,
Downloader downloader) {
if (action.getData().resourceId != 0) {
return new ResourceBitmapHunter(context, picasso, dispatcher,
cache, stats, action);
}
Uri uri = action.getData().uri;
String scheme = uri.getScheme();
if (SCHEME_CONTENT.equals(scheme)) {
if (Contacts.CONTENT_URI.getHost().equals(uri.getHost()) //
&& !uri.getPathSegments().contains(
Contacts.Photo.CONTENT_DIRECTORY)) {
return new ContactsPhotoBitmapHunter(context, picasso,
dispatcher, cache, stats, action);
} else if (MediaStore.AUTHORITY.equals(uri.getAuthority())) {
return new MediaStoreBitmapHunter(context, picasso, dispatcher,
cache, stats, action);
} else {
return new ContentStreamBitmapHunter(context, picasso,
dispatcher, cache, stats, action);
}
} else if (SCHEME_FILE.equals(scheme)) {
if (!uri.getPathSegments().isEmpty()
&& ANDROID_ASSET.equals(uri.getPathSegments().get(0))) {
return new AssetBitmapHunter(context, picasso, dispatcher,
cache, stats, action);
}
return new FileBitmapHunter(context, picasso, dispatcher, cache,
stats, action);
} else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
return new ResourceBitmapHunter(context, picasso, dispatcher,
cache, stats, action);
} else {
return new NetworkBitmapHunter(picasso, dispatcher, cache, stats,
action, downloader);
}
}
/**
* Lazily create {@link android.graphics.BitmapFactory.Options} based in
* given {@link com.squareup.picasso.Request}, only instantiating them if
* needed.
*/
static BitmapFactory.Options createBitmapOptions(Request data) {
final boolean justBounds = data.hasSize();
final boolean hasConfig = data.config != null;
BitmapFactory.Options options = null;
if (justBounds || hasConfig) {
options = new BitmapFactory.Options();
options.inJustDecodeBounds = justBounds;
if (hasConfig) {
options.inPreferredConfig = data.config;
}
}
return options;
}
static boolean requiresInSampleSize(BitmapFactory.Options options) {
return options != null && options.inJustDecodeBounds;
}
static void calculateInSampleSize(int reqWidth, int reqHeight,
BitmapFactory.Options options) {
calculateInSampleSize(reqWidth, reqHeight, options.outWidth,
options.outHeight, options);
}
static void calculateInSampleSize(int reqWidth, int reqHeight, int width,
int height, BitmapFactory.Options options) {
int sampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int heightRatio = Math.round((float) height
/ (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
sampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
options.inSampleSize = sampleSize;
options.inJustDecodeBounds = false;
}
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 = transformation.transform(result);
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 exifRotation) {
int inWidth = result.getWidth();
int inHeight = result.getHeight();
int drawX = 0;
int drawY = 0;
int drawWidth = inWidth;
int drawHeight = inHeight;
Matrix matrix = new Matrix();
if (data.needsMatrixTransform()) {
int targetWidth = data.targetWidth;
int targetHeight = data.targetHeight;
float targetRotation = data.rotationDegrees;
if (targetRotation != 0) {
if (data.hasRotationPivot) {
matrix.setRotate(targetRotation, data.rotationPivotX,
data.rotationPivotY);
} else {
matrix.setRotate(targetRotation);
}
}
if (data.centerCrop) {
float widthRatio = targetWidth / (float) inWidth;
float heightRatio = targetHeight / (float) inHeight;
float scale;
if (widthRatio > heightRatio) {
scale = widthRatio;
int newSize = (int) Math.ceil(inHeight
* (heightRatio / widthRatio));
drawY = (inHeight - newSize) / 2;
drawHeight = newSize;
} else {
scale = heightRatio;
int newSize = (int) Math.ceil(inWidth
* (widthRatio / heightRatio));
drawX = (inWidth - newSize) / 2;
drawWidth = newSize;
}
matrix.preScale(scale, scale);
} else if (data.centerInside) {
float widthRatio = targetWidth / (float) inWidth;
float heightRatio = targetHeight / (float) inHeight;
float scale = widthRatio < heightRatio ? widthRatio
: heightRatio;
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.
float sx = targetWidth / (float) inWidth;
float sy = targetHeight / (float) inHeight;
matrix.preScale(sx, sy);
}
}
if (exifRotation != 0) {
matrix.preRotate(exifRotation);
}
Bitmap newResult = Bitmap.createBitmap(result, drawX, drawY, drawWidth,
drawHeight, matrix, true);
if (newResult != result) {
result.recycle();
result = newResult;
}
return result;
}
}