Bug 963046 - Add Picasso image loading library to the tree (r=nalexander)

This commit is contained in:
Lucas Rocha 2014-02-06 21:06:38 +00:00
parent 9b819f68a5
commit a53b198de4
33 changed files with 4346 additions and 0 deletions

View File

@ -64,6 +64,7 @@ ALL_JARS = \
gecko-browser.jar \
gecko-mozglue.jar \
gecko-util.jar \
squareup-picasso.jar \
sync-thirdparty.jar \
websockets.jar \
$(NULL)

View File

@ -408,11 +408,48 @@ gbjar.generated_sources += sync_generated_java_files
gbjar.extra_jars = [
'gecko-mozglue.jar',
'gecko-util.jar',
'squareup-picasso.jar',
'sync-thirdparty.jar',
'websockets.jar',
]
gbjar.javac_flags += ['-Xlint:all,-deprecation,-fallthrough']
spjar = add_java_jar('squareup-picasso')
spjar.sources += [ thirdparty_source_dir + f for f in [
'com/squareup/picasso/Action.java',
'com/squareup/picasso/AssetBitmapHunter.java',
'com/squareup/picasso/BitmapHunter.java',
'com/squareup/picasso/Cache.java',
'com/squareup/picasso/Callback.java',
'com/squareup/picasso/ContactsPhotoBitmapHunter.java',
'com/squareup/picasso/ContentStreamBitmapHunter.java',
'com/squareup/picasso/DeferredRequestCreator.java',
'com/squareup/picasso/Dispatcher.java',
'com/squareup/picasso/Downloader.java',
'com/squareup/picasso/FetchAction.java',
'com/squareup/picasso/FileBitmapHunter.java',
'com/squareup/picasso/GetAction.java',
'com/squareup/picasso/ImageViewAction.java',
'com/squareup/picasso/LruCache.java',
'com/squareup/picasso/MarkableInputStream.java',
'com/squareup/picasso/MediaStoreBitmapHunter.java',
'com/squareup/picasso/NetworkBitmapHunter.java',
'com/squareup/picasso/Picasso.java',
'com/squareup/picasso/PicassoDrawable.java',
'com/squareup/picasso/PicassoExecutorService.java',
'com/squareup/picasso/Request.java',
'com/squareup/picasso/RequestCreator.java',
'com/squareup/picasso/ResourceBitmapHunter.java',
'com/squareup/picasso/Stats.java',
'com/squareup/picasso/StatsSnapshot.java',
'com/squareup/picasso/Target.java',
'com/squareup/picasso/TargetAction.java',
'com/squareup/picasso/Transformation.java',
'com/squareup/picasso/UrlConnectionDownloader.java',
'com/squareup/picasso/Utils.java',
] ]
#spjar.javac_flags += ['-Xlint:all']
ANDROID_RES_DIRS += [
SRCDIR + '/resources',
TOPSRCDIR + '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res',

View File

@ -0,0 +1,83 @@
/*
* 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.drawable.Drawable;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
abstract class Action<T> {
static class RequestWeakReference<T> extends WeakReference<T> {
final Action action;
public RequestWeakReference(Action action, T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.action = action;
}
}
final Picasso picasso;
final Request data;
final WeakReference<T> target;
final boolean skipCache;
final boolean noFade;
final int errorResId;
final Drawable errorDrawable;
final String key;
boolean cancelled;
Action(Picasso picasso, T target, Request data, boolean skipCache, boolean noFade,
int errorResId, Drawable errorDrawable, String key) {
this.picasso = picasso;
this.data = data;
this.target = new RequestWeakReference<T>(this, target, picasso.referenceQueue);
this.skipCache = skipCache;
this.noFade = noFade;
this.errorResId = errorResId;
this.errorDrawable = errorDrawable;
this.key = key;
}
abstract void complete(Bitmap result, Picasso.LoadedFrom from);
abstract void error();
void cancel() {
cancelled = true;
}
Request getData() {
return data;
}
T getTarget() {
return target.get();
}
String getKey() {
return key;
}
boolean isCancelled() {
return cancelled;
}
Picasso getPicasso() {
return picasso;
}
}

View File

@ -0,0 +1,51 @@
package com.squareup.picasso;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.IOException;
import java.io.InputStream;
import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
class AssetBitmapHunter extends BitmapHunter {
private AssetManager assetManager;
public AssetBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
Stats stats, Action action) {
super(picasso, dispatcher, cache, stats, action);
assetManager = context.getAssets();
}
@Override Bitmap decode(Request data) throws IOException {
String filePath = data.uri.toString().substring(ASSET_PREFIX_LENGTH);
return decodeAsset(filePath);
}
@Override Picasso.LoadedFrom getLoadedFrom() {
return DISK;
}
Bitmap decodeAsset(String filePath) throws IOException {
BitmapFactory.Options options = null;
if (data.hasSize()) {
options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
InputStream is = null;
try {
is = assetManager.open(filePath);
BitmapFactory.decodeStream(is, null, options);
} finally {
Utils.closeQuietly(is);
}
calculateInSampleSize(data.targetWidth, data.targetHeight, options);
}
InputStream is = assetManager.open(filePath);
try {
return BitmapFactory.decodeStream(is, null, options);
} finally {
Utils.closeQuietly(is);
}
}
}

View File

@ -0,0 +1,357 @@
/*
* 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.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.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 String ANDROID_ASSET = "android_asset";
protected static final int ASSET_PREFIX_LENGTH =
(SCHEME_FILE + ":///" + ANDROID_ASSET + "/").length();
final Picasso picasso;
final Dispatcher dispatcher;
final Cache cache;
final Stats stats;
final String key;
final Request data;
final List<Action> actions;
final boolean skipMemoryCache;
Bitmap result;
Future<?> future;
Picasso.LoadedFrom loadedFrom;
Exception exception;
int exifRotation; // Determined during decoding of original resource.
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 {
Thread.currentThread().setName(Utils.THREAD_PREFIX + data.getName());
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);
}
}
stats.dispatchBitmapTransformed(bitmap);
}
}
return bitmap;
}
void attach(Action action) {
actions.add(action);
}
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;
}
List<Action> getActions() {
return actions;
}
Exception getException() {
return exception;
}
Picasso.LoadedFrom getLoadedFrom() {
return loadedFrom;
}
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);
}
}
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;
}
}

View File

@ -0,0 +1,64 @@
/*
* 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;
/**
* A memory cache for storing the most recently used images.
* <p/>
* <em>Note:</em> The {@link Cache} is accessed by multiple threads. You must ensure
* your {@link Cache} implementation is thread safe when {@link Cache#get(String)} or {@link
* Cache#set(String, android.graphics.Bitmap)} is called.
*/
public interface Cache {
/** Retrieve an image for the specified {@code key} or {@code null}. */
Bitmap get(String key);
/** Store an image in the cache for the specified {@code key}. */
void set(String key, Bitmap bitmap);
/** Returns the current size of the cache in bytes. */
int size();
/** Returns the maximum size in bytes that the cache can hold. */
int maxSize();
/** Clears the cache. */
void clear();
/** A cache which does not store any values. */
Cache NONE = new Cache() {
@Override public Bitmap get(String key) {
return null;
}
@Override public void set(String key, Bitmap bitmap) {
// Ignore.
}
@Override public int size() {
return 0;
}
@Override public int maxSize() {
return 0;
}
@Override public void clear() {
}
};
}

View File

@ -0,0 +1,31 @@
/*
* 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;
public interface Callback {
void onSuccess();
void onError();
public static class EmptyCallback implements Callback {
@Override public void onSuccess() {
}
@Override public void onError() {
}
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.UriMatcher;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.ContactsContract;
import java.io.IOException;
import java.io.InputStream;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
import static android.provider.ContactsContract.Contacts.openContactPhotoInputStream;
import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
class ContactsPhotoBitmapHunter extends BitmapHunter {
/** A lookup uri (e.g. content://com.android.contacts/contacts/lookup/3570i61d948d30808e537) */
private static final int ID_LOOKUP = 1;
/** A contact thumbnail uri (e.g. content://com.android.contacts/contacts/38/photo) */
private static final int ID_THUMBNAIL = 2;
/** A contact uri (e.g. content://com.android.contacts/contacts/38) */
private static final int ID_CONTACT = 3;
/**
* A contact display photo (high resolution) uri
* (e.g. content://com.android.contacts/display_photo/5)
*/
private static final int ID_DISPLAY_PHOTO = 4;
private static final UriMatcher matcher;
static {
matcher = new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", ID_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", ID_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", ID_THUMBNAIL);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", ID_CONTACT);
matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", ID_DISPLAY_PHOTO);
}
final Context context;
ContactsPhotoBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
Stats stats, Action action) {
super(picasso, dispatcher, cache, stats, action);
this.context = context;
}
@Override Bitmap decode(Request data) throws IOException {
InputStream is = null;
try {
is = getInputStream();
return decodeStream(is, data);
} finally {
Utils.closeQuietly(is);
}
}
@Override Picasso.LoadedFrom getLoadedFrom() {
return DISK;
}
private InputStream getInputStream() throws IOException {
ContentResolver contentResolver = context.getContentResolver();
Uri uri = getData().uri;
switch (matcher.match(uri)) {
case ID_LOOKUP:
uri = ContactsContract.Contacts.lookupContact(contentResolver, uri);
if (uri == null) {
return null;
}
// Resolved the uri to a contact uri, intentionally fall through to process the resolved uri
case ID_CONTACT:
if (SDK_INT < ICE_CREAM_SANDWICH) {
return openContactPhotoInputStream(contentResolver, uri);
} else {
return ContactPhotoStreamIcs.get(contentResolver, uri);
}
case ID_THUMBNAIL:
case ID_DISPLAY_PHOTO:
return contentResolver.openInputStream(uri);
default:
throw new IllegalStateException("Invalid uri: " + uri);
}
}
private Bitmap decodeStream(InputStream stream, Request data) throws IOException {
if (stream == null) {
return null;
}
BitmapFactory.Options options = null;
if (data.hasSize()) {
options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
InputStream is = getInputStream();
try {
BitmapFactory.decodeStream(is, null, options);
} finally {
Utils.closeQuietly(is);
}
calculateInSampleSize(data.targetWidth, data.targetHeight, options);
}
return BitmapFactory.decodeStream(stream, null, options);
}
@TargetApi(ICE_CREAM_SANDWICH)
private static class ContactPhotoStreamIcs {
static InputStream get(ContentResolver contentResolver, Uri uri) {
return openContactPhotoInputStream(contentResolver, uri, true);
}
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.IOException;
import java.io.InputStream;
import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
class ContentStreamBitmapHunter extends BitmapHunter {
final Context context;
ContentStreamBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
Stats stats, Action action) {
super(picasso, dispatcher, cache, stats, action);
this.context = context;
}
@Override Bitmap decode(Request data)
throws IOException {
return decodeContentStream(data);
}
@Override Picasso.LoadedFrom getLoadedFrom() {
return DISK;
}
protected Bitmap decodeContentStream(Request data) throws IOException {
ContentResolver contentResolver = context.getContentResolver();
BitmapFactory.Options options = null;
if (data.hasSize()) {
options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
InputStream is = null;
try {
is = contentResolver.openInputStream(data.uri);
BitmapFactory.decodeStream(is, null, options);
} finally {
Utils.closeQuietly(is);
}
calculateInSampleSize(data.targetWidth, data.targetHeight, options);
}
InputStream is = contentResolver.openInputStream(data.uri);
try {
return BitmapFactory.decodeStream(is, null, options);
} finally {
Utils.closeQuietly(is);
}
}
}

View File

@ -0,0 +1,70 @@
/*
* 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.view.ViewTreeObserver;
import android.widget.ImageView;
import java.lang.ref.WeakReference;
class DeferredRequestCreator implements ViewTreeObserver.OnPreDrawListener {
final RequestCreator creator;
final WeakReference<ImageView> target;
Callback callback;
DeferredRequestCreator(RequestCreator creator, ImageView target, Callback callback) {
this.creator = creator;
this.target = new WeakReference<ImageView>(target);
this.callback = callback;
target.getViewTreeObserver().addOnPreDrawListener(this);
}
@Override public boolean onPreDraw() {
ImageView target = this.target.get();
if (target == null) {
return true;
}
ViewTreeObserver vto = target.getViewTreeObserver();
if (!vto.isAlive()) {
return true;
}
int width = target.getMeasuredWidth();
int height = target.getMeasuredHeight();
if (width <= 0 || height <= 0) {
return true;
}
vto.removeOnPreDrawListener(this);
this.creator.unfit().resize(width, height).into(target, callback);
return true;
}
void cancel() {
callback = null;
ImageView target = this.target.get();
if (target == null) {
return;
}
ViewTreeObserver vto = target.getViewTreeObserver();
if (!vto.isAlive()) {
return;
}
vto.removeOnPreDrawListener(this);
}
}

View File

@ -0,0 +1,315 @@
/*
* 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.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Intent.ACTION_AIRPLANE_MODE_CHANGED;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.squareup.picasso.BitmapHunter.forRequest;
class Dispatcher {
private static final int RETRY_DELAY = 500;
private static final int AIRPLANE_MODE_ON = 1;
private static final int AIRPLANE_MODE_OFF = 0;
static final int REQUEST_SUBMIT = 1;
static final int REQUEST_CANCEL = 2;
static final int REQUEST_GCED = 3;
static final int HUNTER_COMPLETE = 4;
static final int HUNTER_RETRY = 5;
static final int HUNTER_DECODE_FAILED = 6;
static final int HUNTER_DELAY_NEXT_BATCH = 7;
static final int HUNTER_BATCH_COMPLETE = 8;
static final int NETWORK_STATE_CHANGE = 9;
static final int AIRPLANE_MODE_CHANGE = 10;
private static final String DISPATCHER_THREAD_NAME = "Dispatcher";
private static final int BATCH_DELAY = 200; // ms
final DispatcherThread dispatcherThread;
final Context context;
final ExecutorService service;
final Downloader downloader;
final Map<String, BitmapHunter> hunterMap;
final Handler handler;
final Handler mainThreadHandler;
final Cache cache;
final Stats stats;
final List<BitmapHunter> batch;
final NetworkBroadcastReceiver receiver;
NetworkInfo networkInfo;
boolean airplaneMode;
Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
Downloader downloader, Cache cache, Stats stats) {
this.dispatcherThread = new DispatcherThread();
this.dispatcherThread.start();
this.context = context;
this.service = service;
this.hunterMap = new LinkedHashMap<String, BitmapHunter>();
this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
this.downloader = downloader;
this.mainThreadHandler = mainThreadHandler;
this.cache = cache;
this.stats = stats;
this.batch = new ArrayList<BitmapHunter>(4);
this.airplaneMode = Utils.isAirplaneModeOn(this.context);
this.receiver = new NetworkBroadcastReceiver(this.context);
receiver.register();
}
void shutdown() {
service.shutdown();
dispatcherThread.quit();
receiver.unregister();
}
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
void dispatchCancel(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_CANCEL, action));
}
void dispatchComplete(BitmapHunter hunter) {
handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));
}
void dispatchRetry(BitmapHunter hunter) {
handler.sendMessageDelayed(handler.obtainMessage(HUNTER_RETRY, hunter), RETRY_DELAY);
}
void dispatchFailed(BitmapHunter hunter) {
handler.sendMessage(handler.obtainMessage(HUNTER_DECODE_FAILED, hunter));
}
void dispatchNetworkStateChange(NetworkInfo info) {
handler.sendMessage(handler.obtainMessage(NETWORK_STATE_CHANGE, info));
}
void dispatchAirplaneModeChange(boolean airplaneMode) {
handler.sendMessage(handler.obtainMessage(AIRPLANE_MODE_CHANGE,
airplaneMode ? AIRPLANE_MODE_ON : AIRPLANE_MODE_OFF, 0));
}
void performSubmit(Action action) {
BitmapHunter hunter = hunterMap.get(action.getKey());
if (hunter != null) {
hunter.attach(action);
return;
}
if (service.isShutdown()) {
return;
}
hunter = forRequest(context, action.getPicasso(), this, cache, stats, action, downloader);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
}
void performCancel(Action action) {
String key = action.getKey();
BitmapHunter hunter = hunterMap.get(key);
if (hunter != null) {
hunter.detach(action);
if (hunter.cancel()) {
hunterMap.remove(key);
}
}
}
void performRetry(BitmapHunter hunter) {
if (hunter.isCancelled()) return;
if (service.isShutdown()) {
performError(hunter);
return;
}
if (hunter.shouldRetry(airplaneMode, networkInfo)) {
hunter.future = service.submit(hunter);
} else {
performError(hunter);
}
}
void performComplete(BitmapHunter hunter) {
if (!hunter.shouldSkipMemoryCache()) {
cache.set(hunter.getKey(), hunter.getResult());
}
hunterMap.remove(hunter.getKey());
batch(hunter);
}
void performBatchComplete() {
List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);
batch.clear();
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
}
void performError(BitmapHunter hunter) {
hunterMap.remove(hunter.getKey());
batch(hunter);
}
void performAirplaneModeChange(boolean airplaneMode) {
this.airplaneMode = airplaneMode;
}
void performNetworkStateChange(NetworkInfo info) {
networkInfo = info;
if (service instanceof PicassoExecutorService) {
((PicassoExecutorService) service).adjustThreadCount(info);
}
}
private void batch(BitmapHunter hunter) {
if (hunter.isCancelled()) {
return;
}
batch.add(hunter);
if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
}
}
private static class DispatcherHandler extends Handler {
private final Dispatcher dispatcher;
public DispatcherHandler(Looper looper, Dispatcher dispatcher) {
super(looper);
this.dispatcher = dispatcher;
}
@Override public void handleMessage(final Message msg) {
switch (msg.what) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
dispatcher.performSubmit(action);
break;
}
case REQUEST_CANCEL: {
Action action = (Action) msg.obj;
dispatcher.performCancel(action);
break;
}
case HUNTER_COMPLETE: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
dispatcher.performComplete(hunter);
break;
}
case HUNTER_RETRY: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
dispatcher.performRetry(hunter);
break;
}
case HUNTER_DECODE_FAILED: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
dispatcher.performError(hunter);
break;
}
case HUNTER_DELAY_NEXT_BATCH: {
dispatcher.performBatchComplete();
break;
}
case NETWORK_STATE_CHANGE: {
NetworkInfo info = (NetworkInfo) msg.obj;
dispatcher.performNetworkStateChange(info);
break;
}
case AIRPLANE_MODE_CHANGE: {
dispatcher.performAirplaneModeChange(msg.arg1 == AIRPLANE_MODE_ON);
break;
}
default:
Picasso.HANDLER.post(new Runnable() {
@Override public void run() {
throw new AssertionError("Unknown handler message received: " + msg.what);
}
});
}
}
}
static class DispatcherThread extends HandlerThread {
DispatcherThread() {
super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
}
}
private class NetworkBroadcastReceiver extends BroadcastReceiver {
private static final String EXTRA_AIRPLANE_STATE = "state";
private final ConnectivityManager connectivityManager;
NetworkBroadcastReceiver(Context context) {
connectivityManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
}
void register() {
boolean shouldScanState = service instanceof PicassoExecutorService && //
Utils.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE);
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_AIRPLANE_MODE_CHANGED);
if (shouldScanState) {
filter.addAction(CONNECTIVITY_ACTION);
}
context.registerReceiver(this, filter);
}
void unregister() {
context.unregisterReceiver(this);
}
@Override public void onReceive(Context context, Intent intent) {
// On some versions of Android this may be called with a null Intent
if (null == intent) {
return;
}
String action = intent.getAction();
Bundle extras = intent.getExtras();
if (ACTION_AIRPLANE_MODE_CHANGED.equals(action)) {
dispatchAirplaneModeChange(extras.getBoolean(EXTRA_AIRPLANE_STATE, false));
} else if (CONNECTIVITY_ACTION.equals(action)) {
dispatchNetworkStateChange(connectivityManager.getActiveNetworkInfo());
}
}
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.net.Uri;
import java.io.IOException;
import java.io.InputStream;
/** A mechanism to load images from external resources such as a disk cache and/or the internet. */
public interface Downloader {
/**
* Download the specified image {@code url} from the internet.
*
* @param uri Remote image URL.
* @param localCacheOnly If {@code true} the URL should only be loaded if available in a local
* disk cache.
* @return {@link Response} containing either a {@link Bitmap} representation of the request or an
* {@link InputStream} for the image data. {@code null} can be returned to indicate a problem
* loading the bitmap.
* @throws IOException if the requested URL cannot successfully be loaded.
*/
Response load(Uri uri, boolean localCacheOnly) throws IOException;
/** Thrown for non-2XX responses. */
class ResponseException extends IOException {
public ResponseException(String message) {
super(message);
}
}
/** Response stream or bitmap and info. */
class Response {
final InputStream stream;
final Bitmap bitmap;
final boolean cached;
/**
* Response image and info.
*
* @param bitmap Image.
* @param loadedFromCache {@code true} if the source of the image is from a local disk cache.
*/
public Response(Bitmap bitmap, boolean loadedFromCache) {
if (bitmap == null) {
throw new IllegalArgumentException("Bitmap may not be null.");
}
this.stream = null;
this.bitmap = bitmap;
this.cached = loadedFromCache;
}
/**
* Response stream and info.
*
* @param stream Image data stream.
* @param loadedFromCache {@code true} if the source of the stream is from a local disk cache.
*/
public Response(InputStream stream, boolean loadedFromCache) {
if (stream == null) {
throw new IllegalArgumentException("Stream may not be null.");
}
this.stream = stream;
this.bitmap = null;
this.cached = loadedFromCache;
}
/**
* Input stream containing image data.
* <p>
* If this returns {@code null}, image data will be available via {@link #getBitmap()}.
*/
public InputStream getInputStream() {
return stream;
}
/**
* Bitmap representing the image.
* <p>
* If this returns {@code null}, image data will be available via {@link #getInputStream()}.
*/
public Bitmap getBitmap() {
return bitmap;
}
}
}

View File

@ -0,0 +1,30 @@
/*
* 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;
class FetchAction extends Action<Void> {
FetchAction(Picasso picasso, Request data, boolean skipCache, String key) {
super(picasso, null, data, skipCache, false, 0, null, key);
}
@Override void complete(Bitmap result, Picasso.LoadedFrom from) {
}
@Override public void error() {
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.content.Context;
import android.graphics.Bitmap;
import android.media.ExifInterface;
import android.net.Uri;
import java.io.IOException;
import static android.media.ExifInterface.ORIENTATION_NORMAL;
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.TAG_ORIENTATION;
class FileBitmapHunter extends ContentStreamBitmapHunter {
FileBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
Stats stats, Action action) {
super(context, picasso, dispatcher, cache, stats, action);
}
@Override Bitmap decode(Request data)
throws IOException {
setExifRotation(getFileExifRotation(data.uri));
return super.decode(data);
}
static int getFileExifRotation(Uri uri) throws IOException {
ExifInterface exifInterface = new ExifInterface(uri.getPath());
int orientation = exifInterface.getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
switch (orientation) {
case ORIENTATION_ROTATE_90:
return 90;
case ORIENTATION_ROTATE_180:
return 180;
case ORIENTATION_ROTATE_270:
return 270;
default:
return 0;
}
}
}

View File

@ -0,0 +1,30 @@
/*
* 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;
class GetAction extends Action<Void> {
GetAction(Picasso picasso, Request data, boolean skipCache, String key) {
super(picasso, null, data, skipCache, false, 0, null, key);
}
@Override void complete(Bitmap result, Picasso.LoadedFrom from) {
}
@Override public void error() {
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
class ImageViewAction extends Action<ImageView> {
Callback callback;
ImageViewAction(Picasso picasso, ImageView imageView, Request data, boolean skipCache,
boolean noFade, int errorResId, Drawable errorDrawable, String key, Callback callback) {
super(picasso, imageView, data, skipCache, noFade, errorResId, errorDrawable, key);
this.callback = callback;
}
@Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
if (result == null) {
throw new AssertionError(
String.format("Attempted to complete action with no result!\n%s", this));
}
ImageView target = this.target.get();
if (target == null) {
return;
}
Context context = picasso.context;
boolean debugging = picasso.debugging;
PicassoDrawable.setBitmap(target, context, result, from, noFade, debugging);
if (callback != null) {
callback.onSuccess();
}
}
@Override public void error() {
ImageView target = this.target.get();
if (target == null) {
return;
}
if (errorResId != 0) {
target.setImageResource(errorResId);
} else if (errorDrawable != null) {
target.setImageDrawable(errorDrawable);
}
if (callback != null) {
callback.onError();
}
}
@Override void cancel() {
super.cancel();
if (callback != null) {
callback = null;
}
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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.content.Context;
import android.graphics.Bitmap;
import java.util.LinkedHashMap;
import java.util.Map;
/** A memory cache which uses a least-recently used eviction policy. */
public class LruCache implements Cache {
final LinkedHashMap<String, Bitmap> map;
private final int maxSize;
private int size;
private int putCount;
private int evictionCount;
private int hitCount;
private int missCount;
/** Create a cache using an appropriate portion of the available RAM as the maximum size. */
public LruCache(Context context) {
this(Utils.calculateMemoryCacheSize(context));
}
/** Create a cache with a given maximum size in bytes. */
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("Max size must be positive.");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
}
@Override public Bitmap get(String key) {
if (key == null) {
throw new NullPointerException("key == null");
}
Bitmap mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
return null;
}
@Override public void set(String key, Bitmap bitmap) {
if (key == null || bitmap == null) {
throw new NullPointerException("key == null || bitmap == null");
}
Bitmap previous;
synchronized (this) {
putCount++;
size += Utils.getBitmapBytes(bitmap);
previous = map.put(key, bitmap);
if (previous != null) {
size -= Utils.getBitmapBytes(previous);
}
}
trimToSize(maxSize);
}
private void trimToSize(int maxSize) {
while (true) {
String key;
Bitmap value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(
getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= Utils.getBitmapBytes(value);
evictionCount++;
}
}
}
/** Clear the cache. */
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
/** Returns the sum of the sizes of the entries in this cache. */
public final synchronized int size() {
return size;
}
/** Returns the maximum sum of the sizes of the entries in this cache. */
public final synchronized int maxSize() {
return maxSize;
}
public final synchronized void clear() {
evictAll();
}
/** Returns the number of times {@link #get} returned a value. */
public final synchronized int hitCount() {
return hitCount;
}
/** Returns the number of times {@link #get} returned {@code null}. */
public final synchronized int missCount() {
return missCount;
}
/** Returns the number of times {@link #set(String, Bitmap)} was called. */
public final synchronized int putCount() {
return putCount;
}
/** Returns the number of values that have been evicted. */
public final synchronized int evictionCount() {
return evictionCount;
}
}

View File

@ -0,0 +1,157 @@
/*
* 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 java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* An input stream wrapper that supports unlimited independent cursors for
* marking and resetting. Each cursor is a token, and it's the caller's
* responsibility to keep track of these.
*/
final class MarkableInputStream extends InputStream {
private final InputStream in;
private long offset;
private long reset;
private long limit;
private long defaultMark = -1;
public MarkableInputStream(InputStream in) {
if (!in.markSupported()) {
in = new BufferedInputStream(in);
}
this.in = in;
}
/** Marks this place in the stream so we can reset back to it later. */
@Override public void mark(int readLimit) {
defaultMark = savePosition(readLimit);
}
/**
* Returns an opaque token representing the current position in the stream.
* Call {@link #reset(long)} to return to this position in the stream later.
* It is an error to call {@link #reset(long)} after consuming more than
* {@code readLimit} bytes from this stream.
*/
public long savePosition(int readLimit) {
long offsetLimit = offset + readLimit;
if (limit < offsetLimit) {
setLimit(offsetLimit);
}
return offset;
}
/**
* Makes sure that the underlying stream can backtrack the full range from
* {@code reset} thru {@code limit}. Since we can't call {@code mark()}
* without also adjusting the reset-to-position on the underlying stream this
* method resets first and then marks the union of the two byte ranges. On
* buffered streams this additional cursor motion shouldn't result in any
* additional I/O.
*/
private void setLimit(long limit) {
try {
if (reset < offset && offset <= this.limit) {
in.reset();
in.mark((int) (limit - reset));
skip(reset, offset);
} else {
reset = offset;
in.mark((int) (limit - offset));
}
this.limit = limit;
} catch (IOException e) {
throw new IllegalStateException("Unable to mark: " + e);
}
}
/** Resets the stream to the most recent {@link #mark mark}. */
@Override public void reset() throws IOException {
reset(defaultMark);
}
/** Resets the stream to the position recorded by {@code token}. */
public void reset(long token) throws IOException {
if (offset > limit || token < reset) {
throw new IOException("Cannot reset");
}
in.reset();
skip(reset, token);
offset = token;
}
/** Skips {@code target - current} bytes and returns. */
private void skip(long current, long target) throws IOException {
while (current < target) {
long skipped = in.skip(target - current);
if (skipped == 0) {
if (read() == -1) {
break; // EOF
} else {
skipped = 1;
}
}
current += skipped;
}
}
@Override public int read() throws IOException {
int result = in.read();
if (result != -1) {
offset++;
}
return result;
}
@Override public int read(byte[] buffer) throws IOException {
int count = in.read(buffer);
if (count != -1) {
offset += count;
}
return count;
}
@Override public int read(byte[] buffer, int offset, int length) throws IOException {
int count = in.read(buffer, offset, length);
if (count != -1) {
this.offset += count;
}
return count;
}
@Override public long skip(long byteCount) throws IOException {
long skipped = in.skip(byteCount);
offset += skipped;
return skipped;
}
@Override public int available() throws IOException {
return in.available();
}
@Override public void close() throws IOException {
in.close();
}
@Override public boolean markSupported() {
return in.markSupported();
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (C) 2014 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.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import java.io.IOException;
import static android.content.ContentUris.parseId;
import static android.provider.MediaStore.Images.Thumbnails.FULL_SCREEN_KIND;
import static android.provider.MediaStore.Images.Thumbnails.MICRO_KIND;
import static android.provider.MediaStore.Images.Thumbnails.MINI_KIND;
import static android.provider.MediaStore.Images.Thumbnails.getThumbnail;
import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.FULL;
import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.MICRO;
import static com.squareup.picasso.MediaStoreBitmapHunter.PicassoKind.MINI;
class MediaStoreBitmapHunter extends ContentStreamBitmapHunter {
private static final String[] CONTENT_ORIENTATION = new String[] {
MediaStore.Images.ImageColumns.ORIENTATION
};
MediaStoreBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
Stats stats, Action action) {
super(context, picasso, dispatcher, cache, stats, action);
}
@Override Bitmap decode(Request data) throws IOException {
ContentResolver contentResolver = context.getContentResolver();
setExifRotation(getExitOrientation(contentResolver, data.uri));
if (data.hasSize()) {
PicassoKind picassoKind = getPicassoKind(data.targetWidth, data.targetHeight);
if (picassoKind == FULL) {
return super.decode(data);
}
long id = parseId(data.uri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
calculateInSampleSize(data.targetWidth, data.targetHeight, picassoKind.width,
picassoKind.height, options);
Bitmap result = getThumbnail(contentResolver, id, picassoKind.androidKind, options);
if (result != null) {
return result;
}
}
return super.decode(data);
}
static PicassoKind getPicassoKind(int targetWidth, int targetHeight) {
if (targetWidth <= MICRO.width && targetHeight <= MICRO.height) {
return MICRO;
} else if (targetWidth <= MINI.width && targetHeight <= MINI.height) {
return MINI;
}
return FULL;
}
static int getExitOrientation(ContentResolver contentResolver, Uri uri) {
Cursor cursor = null;
try {
cursor = contentResolver.query(uri, CONTENT_ORIENTATION, null, null, null);
if (cursor == null || !cursor.moveToFirst()) {
return 0;
}
return cursor.getInt(0);
} catch (RuntimeException ignored) {
// If the orientation column doesn't exist, assume no rotation.
return 0;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
enum PicassoKind {
MICRO(MICRO_KIND, 96, 96),
MINI(MINI_KIND, 512, 384),
FULL(FULL_SCREEN_KIND, -1, -1);
final int androidKind;
final int width;
final int height;
PicassoKind(int androidKind, int width, int height) {
this.androidKind = androidKind;
this.width = width;
this.height = height;
}
}
}

View File

@ -0,0 +1,113 @@
/*
* 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.net.NetworkInfo;
import java.io.IOException;
import java.io.InputStream;
import static com.squareup.picasso.Downloader.Response;
import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
import static com.squareup.picasso.Picasso.LoadedFrom.NETWORK;
class NetworkBitmapHunter extends BitmapHunter {
static final int DEFAULT_RETRY_COUNT = 2;
private static final int MARKER = 65536;
private final Downloader downloader;
int retryCount;
public NetworkBitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
Action action, Downloader downloader) {
super(picasso, dispatcher, cache, stats, action);
this.downloader = downloader;
this.retryCount = DEFAULT_RETRY_COUNT;
}
@Override Bitmap decode(Request data) throws IOException {
boolean loadFromLocalCacheOnly = retryCount == 0;
Response response = downloader.load(data.uri, loadFromLocalCacheOnly);
if (response == null) {
return null;
}
loadedFrom = response.cached ? DISK : NETWORK;
Bitmap result = response.getBitmap();
if (result != null) {
return result;
}
InputStream is = response.getInputStream();
try {
return decodeStream(is, data);
} finally {
Utils.closeQuietly(is);
}
}
@Override boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {
boolean hasRetries = retryCount > 0;
if (!hasRetries) {
return false;
}
retryCount--;
return info == null || info.isConnectedOrConnecting();
}
private Bitmap decodeStream(InputStream stream, Request data) throws IOException {
if (stream == null) {
return null;
}
MarkableInputStream markStream = new MarkableInputStream(stream);
stream = markStream;
long mark = markStream.savePosition(MARKER);
boolean isWebPFile = Utils.isWebPFile(stream);
markStream.reset(mark);
// When decode WebP network stream, BitmapFactory throw JNI Exception and make app crash.
// Decode byte array instead
if (isWebPFile) {
byte[] bytes = Utils.toByteArray(stream);
BitmapFactory.Options options = null;
if (data.hasSize()) {
options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
calculateInSampleSize(data.targetWidth, data.targetHeight, options);
}
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
} else {
BitmapFactory.Options options = null;
if (data.hasSize()) {
options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(stream, null, options);
calculateInSampleSize(data.targetWidth, data.targetHeight, options);
markStream.reset(mark);
}
return BitmapFactory.decodeStream(stream, null, options);
}
}
}

View File

@ -0,0 +1,522 @@
/*
* 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.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.widget.ImageView;
import java.io.File;
import java.lang.ref.ReferenceQueue;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.squareup.picasso.Action.RequestWeakReference;
import static com.squareup.picasso.Dispatcher.HUNTER_BATCH_COMPLETE;
import static com.squareup.picasso.Dispatcher.REQUEST_GCED;
import static com.squareup.picasso.Utils.THREAD_PREFIX;
/**
* Image downloading, transformation, and caching manager.
* <p/>
* Use {@link #with(android.content.Context)} for the global singleton instance or construct your
* own instance with {@link Builder}.
*/
public class Picasso {
/** Callbacks for Picasso events. */
public interface Listener {
/**
* Invoked when an image has failed to load. This is useful for reporting image failures to a
* remote analytics service, for example.
*/
void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception);
}
/**
* A transformer that is called immediately before every request is submitted. This can be used to
* modify any information about a request.
* <p>
* For example, if you use a CDN you can change the hostname for the image based on the current
* location of the user in order to get faster download speeds.
* <p>
* <b>NOTE:</b> This is a beta feature. The API is subject to change in a backwards incompatible
* way at any time.
*/
public interface RequestTransformer {
/**
* Transform a request before it is submitted to be processed.
*
* @return The original request or a new request to replace it. Must not be null.
*/
Request transformRequest(Request request);
/** A {@link RequestTransformer} which returns the original request. */
RequestTransformer IDENTITY = new RequestTransformer() {
@Override public Request transformRequest(Request request) {
return request;
}
};
}
static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
@Override public void handleMessage(Message msg) {
switch (msg.what) {
case HUNTER_BATCH_COMPLETE: {
@SuppressWarnings("unchecked") List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj;
for (BitmapHunter hunter : batch) {
hunter.picasso.complete(hunter);
}
break;
}
case REQUEST_GCED: {
Action action = (Action) msg.obj;
action.picasso.cancelExistingRequest(action.getTarget());
break;
}
default:
throw new AssertionError("Unknown handler message received: " + msg.what);
}
}
};
static Picasso singleton = null;
private final Listener listener;
private final RequestTransformer requestTransformer;
private final CleanupThread cleanupThread;
final Context context;
final Dispatcher dispatcher;
final Cache cache;
final Stats stats;
final Map<Object, Action> targetToAction;
final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator;
final ReferenceQueue<Object> referenceQueue;
boolean debugging;
boolean shutdown;
Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
RequestTransformer requestTransformer, Stats stats, boolean debugging) {
this.context = context;
this.dispatcher = dispatcher;
this.cache = cache;
this.listener = listener;
this.requestTransformer = requestTransformer;
this.stats = stats;
this.targetToAction = new WeakHashMap<Object, Action>();
this.targetToDeferredRequestCreator = new WeakHashMap<ImageView, DeferredRequestCreator>();
this.debugging = debugging;
this.referenceQueue = new ReferenceQueue<Object>();
this.cleanupThread = new CleanupThread(referenceQueue, HANDLER);
this.cleanupThread.start();
}
/** Cancel any existing requests for the specified target {@link ImageView}. */
public void cancelRequest(ImageView view) {
cancelExistingRequest(view);
}
/** Cancel any existing requests for the specified {@link Target} instance. */
public void cancelRequest(Target target) {
cancelExistingRequest(target);
}
/**
* Start an image request using the specified URI.
* <p>
* Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,
* if one is specified.
*
* @see #load(File)
* @see #load(String)
* @see #load(int)
*/
public RequestCreator load(Uri uri) {
return new RequestCreator(this, uri, 0);
}
/**
* Start an image request using the specified path. This is a convenience method for calling
* {@link #load(Uri)}.
* <p>
* This path may be a remote URL, file resource (prefixed with {@code file:}), content resource
* (prefixed with {@code content:}), or android resource (prefixed with {@code
* android.resource:}.
* <p>
* Passing {@code null} as a {@code path} will not trigger any request but will set a
* placeholder, if one is specified.
*
* @see #load(Uri)
* @see #load(File)
* @see #load(int)
*/
public RequestCreator load(String path) {
if (path == null) {
return new RequestCreator(this, null, 0);
}
if (path.trim().length() == 0) {
throw new IllegalArgumentException("Path must not be empty.");
}
return load(Uri.parse(path));
}
/**
* Start an image request using the specified image file. This is a convenience method for
* calling {@link #load(Uri)}.
* <p>
* Passing {@code null} as a {@code file} will not trigger any request but will set a
* placeholder, if one is specified.
*
* @see #load(Uri)
* @see #load(String)
* @see #load(int)
*/
public RequestCreator load(File file) {
if (file == null) {
return new RequestCreator(this, null, 0);
}
return load(Uri.fromFile(file));
}
/**
* Start an image request using the specified drawable resource ID.
*
* @see #load(Uri)
* @see #load(String)
* @see #load(File)
*/
public RequestCreator load(int resourceId) {
if (resourceId == 0) {
throw new IllegalArgumentException("Resource ID must not be zero.");
}
return new RequestCreator(this, null, resourceId);
}
/** {@code true} if debug display, logging, and statistics are enabled. */
@SuppressWarnings("UnusedDeclaration") public boolean isDebugging() {
return debugging;
}
/** Toggle whether debug display, logging, and statistics are enabled. */
@SuppressWarnings("UnusedDeclaration") public void setDebugging(boolean debugging) {
this.debugging = debugging;
}
/** Creates a {@link StatsSnapshot} of the current stats for this instance. */
@SuppressWarnings("UnusedDeclaration") public StatsSnapshot getSnapshot() {
return stats.createSnapshot();
}
/** Stops this instance from accepting further requests. */
public void shutdown() {
if (this == singleton) {
throw new UnsupportedOperationException("Default singleton instance cannot be shutdown.");
}
if (shutdown) {
return;
}
cache.clear();
cleanupThread.shutdown();
stats.shutdown();
dispatcher.shutdown();
for (DeferredRequestCreator deferredRequestCreator : targetToDeferredRequestCreator.values()) {
deferredRequestCreator.cancel();
}
targetToDeferredRequestCreator.clear();
shutdown = true;
}
Request transformRequest(Request request) {
Request transformed = requestTransformer.transformRequest(request);
if (transformed == null) {
throw new IllegalStateException("Request transformer "
+ requestTransformer.getClass().getCanonicalName()
+ " returned null for "
+ request);
}
return transformed;
}
void defer(ImageView view, DeferredRequestCreator request) {
targetToDeferredRequestCreator.put(view, request);
}
void enqueueAndSubmit(Action action) {
Object target = action.getTarget();
if (target != null) {
cancelExistingRequest(target);
targetToAction.put(target, action);
}
submit(action);
}
void submit(Action action) {
dispatcher.dispatchSubmit(action);
}
Bitmap quickMemoryCacheCheck(String key) {
Bitmap cached = cache.get(key);
if (cached != null) {
stats.dispatchCacheHit();
} else {
stats.dispatchCacheMiss();
}
return cached;
}
void complete(BitmapHunter hunter) {
List<Action> joined = hunter.getActions();
if (joined.isEmpty()) {
return;
}
Uri uri = hunter.getData().uri;
Exception exception = hunter.getException();
Bitmap result = hunter.getResult();
LoadedFrom from = hunter.getLoadedFrom();
for (Action join : joined) {
if (join.isCancelled()) {
continue;
}
targetToAction.remove(join.getTarget());
if (result != null) {
if (from == null) {
throw new AssertionError("LoadedFrom cannot be null.");
}
join.complete(result, from);
} else {
join.error();
}
}
if (listener != null && exception != null) {
listener.onImageLoadFailed(this, uri, exception);
}
}
private void cancelExistingRequest(Object target) {
Action action = targetToAction.remove(target);
if (action != null) {
action.cancel();
dispatcher.dispatchCancel(action);
}
if (target instanceof ImageView) {
ImageView targetImageView = (ImageView) target;
DeferredRequestCreator deferredRequestCreator =
targetToDeferredRequestCreator.remove(targetImageView);
if (deferredRequestCreator != null) {
deferredRequestCreator.cancel();
}
}
}
private static class CleanupThread extends Thread {
private final ReferenceQueue<?> referenceQueue;
private final Handler handler;
CleanupThread(ReferenceQueue<?> referenceQueue, Handler handler) {
this.referenceQueue = referenceQueue;
this.handler = handler;
setDaemon(true);
setName(THREAD_PREFIX + "refQueue");
}
@Override public void run() {
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
while (true) {
try {
RequestWeakReference<?> remove = (RequestWeakReference<?>) referenceQueue.remove();
handler.sendMessage(handler.obtainMessage(REQUEST_GCED, remove.action));
} catch (InterruptedException e) {
break;
} catch (final Exception e) {
handler.post(new Runnable() {
@Override public void run() {
throw new RuntimeException(e);
}
});
break;
}
}
}
void shutdown() {
interrupt();
}
}
/**
* The global default {@link Picasso} instance.
* <p>
* This instance is automatically initialized with defaults that are suitable to most
* implementations.
* <ul>
* <li>LRU memory cache of 15% the available application RAM</li>
* <li>Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only
* available on API 14+ <em>or</em> if you are using a standalone library that provides a disk
* cache on all API levels like OkHttp)</li>
* <li>Three download threads for disk and network access.</li>
* </ul>
* <p>
* If these settings do not meet the requirements of your application you can construct your own
* instance with full control over the configuration by using {@link Picasso.Builder}.
*/
public static Picasso with(Context context) {
if (singleton == null) {
singleton = new Builder(context).build();
}
return singleton;
}
/** Fluent API for creating {@link Picasso} instances. */
@SuppressWarnings("UnusedDeclaration") // Public API.
public static class Builder {
private final Context context;
private Downloader downloader;
private ExecutorService service;
private Cache cache;
private Listener listener;
private RequestTransformer transformer;
private boolean debugging;
/** Start building a new {@link Picasso} instance. */
public Builder(Context context) {
if (context == null) {
throw new IllegalArgumentException("Context must not be null.");
}
this.context = context.getApplicationContext();
}
/** Specify the {@link Downloader} that will be used for downloading images. */
public Builder downloader(Downloader downloader) {
if (downloader == null) {
throw new IllegalArgumentException("Downloader must not be null.");
}
if (this.downloader != null) {
throw new IllegalStateException("Downloader already set.");
}
this.downloader = downloader;
return this;
}
/** Specify the executor service for loading images in the background. */
public Builder executor(ExecutorService executorService) {
if (executorService == null) {
throw new IllegalArgumentException("Executor service must not be null.");
}
if (this.service != null) {
throw new IllegalStateException("Executor service already set.");
}
this.service = executorService;
return this;
}
/** Specify the memory cache used for the most recent images. */
public Builder memoryCache(Cache memoryCache) {
if (memoryCache == null) {
throw new IllegalArgumentException("Memory cache must not be null.");
}
if (this.cache != null) {
throw new IllegalStateException("Memory cache already set.");
}
this.cache = memoryCache;
return this;
}
/** Specify a listener for interesting events. */
public Builder listener(Listener listener) {
if (listener == null) {
throw new IllegalArgumentException("Listener must not be null.");
}
if (this.listener != null) {
throw new IllegalStateException("Listener already set.");
}
this.listener = listener;
return this;
}
/**
* Specify a transformer for all incoming requests.
* <p>
* <b>NOTE:</b> This is a beta feature. The API is subject to change in a backwards incompatible
* way at any time.
*/
public Builder requestTransformer(RequestTransformer transformer) {
if (transformer == null) {
throw new IllegalArgumentException("Transformer must not be null.");
}
if (this.transformer != null) {
throw new IllegalStateException("Transformer already set.");
}
this.transformer = transformer;
return this;
}
/** Whether debugging is enabled or not. */
public Builder debugging(boolean debugging) {
this.debugging = debugging;
return this;
}
/** Create the {@link Picasso} instance. */
public Picasso build() {
Context context = this.context;
if (downloader == null) {
downloader = Utils.createDefaultDownloader(context);
}
if (cache == null) {
cache = new LruCache(context);
}
if (service == null) {
service = new PicassoExecutorService();
}
if (transformer == null) {
transformer = RequestTransformer.IDENTITY;
}
Stats stats = new Stats(cache);
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
return new Picasso(context, dispatcher, cache, listener, transformer, stats, debugging);
}
}
/** Describes where the image was loaded from. */
public enum LoadedFrom {
MEMORY(Color.GREEN),
DISK(Color.YELLOW),
NETWORK(Color.RED);
final int debugColor;
private LoadedFrom(int debugColor) {
this.debugColor = debugColor;
}
}
}

View File

@ -0,0 +1,186 @@
/*
* 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.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.widget.ImageView;
import static android.graphics.Color.WHITE;
import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
final class PicassoDrawable extends Drawable {
// Only accessed from main thread.
private static final Paint DEBUG_PAINT = new Paint();
private static final float FADE_DURATION = 200f; //ms
/**
* Create or update the drawable on the target {@link ImageView} to display the supplied bitmap
* image.
*/
static void setBitmap(ImageView target, Context context, Bitmap bitmap,
Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {
Drawable placeholder = target.getDrawable();
if (placeholder instanceof AnimationDrawable) {
((AnimationDrawable) placeholder).stop();
}
PicassoDrawable drawable =
new PicassoDrawable(context, placeholder, bitmap, loadedFrom, noFade, debugging);
target.setImageDrawable(drawable);
}
/**
* Create or update the drawable on the target {@link ImageView} to display the supplied
* placeholder image.
*/
static void setPlaceholder(ImageView target, int placeholderResId, Drawable placeholderDrawable) {
if (placeholderResId != 0) {
target.setImageResource(placeholderResId);
} else {
target.setImageDrawable(placeholderDrawable);
}
if (target.getDrawable() instanceof AnimationDrawable) {
((AnimationDrawable) target.getDrawable()).start();
}
}
private final boolean debugging;
private final float density;
private final Picasso.LoadedFrom loadedFrom;
final BitmapDrawable image;
Drawable placeholder;
long startTimeMillis;
boolean animating;
int alpha = 0xFF;
PicassoDrawable(Context context, Drawable placeholder, Bitmap bitmap,
Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {
Resources res = context.getResources();
this.debugging = debugging;
this.density = res.getDisplayMetrics().density;
this.loadedFrom = loadedFrom;
this.image = new BitmapDrawable(res, bitmap);
boolean fade = loadedFrom != MEMORY && !noFade;
if (fade) {
this.placeholder = placeholder;
animating = true;
startTimeMillis = SystemClock.uptimeMillis();
}
}
@Override public void draw(Canvas canvas) {
if (!animating) {
image.draw(canvas);
} else {
float normalized = (SystemClock.uptimeMillis() - startTimeMillis) / FADE_DURATION;
if (normalized >= 1f) {
animating = false;
placeholder = null;
image.draw(canvas);
} else {
if (placeholder != null) {
placeholder.draw(canvas);
}
int partialAlpha = (int) (alpha * normalized);
image.setAlpha(partialAlpha);
image.draw(canvas);
image.setAlpha(alpha);
invalidateSelf();
}
}
if (debugging) {
drawDebugIndicator(canvas);
}
}
@Override public int getIntrinsicWidth() {
return image.getIntrinsicWidth();
}
@Override public int getIntrinsicHeight() {
return image.getIntrinsicHeight();
}
@Override public void setAlpha(int alpha) {
this.alpha = alpha;
if (placeholder != null) {
placeholder.setAlpha(alpha);
}
image.setAlpha(alpha);
}
@Override public void setColorFilter(ColorFilter cf) {
if (placeholder != null) {
placeholder.setColorFilter(cf);
}
image.setColorFilter(cf);
}
@Override public int getOpacity() {
return image.getOpacity();
}
@Override protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
image.setBounds(bounds);
if (placeholder != null) {
placeholder.setBounds(bounds);
}
}
private void drawDebugIndicator(Canvas canvas) {
DEBUG_PAINT.setColor(WHITE);
Path path = getTrianglePath(new Point(0, 0), (int) (16 * density));
canvas.drawPath(path, DEBUG_PAINT);
DEBUG_PAINT.setColor(loadedFrom.debugColor);
path = getTrianglePath(new Point(0, 0), (int) (15 * density));
canvas.drawPath(path, DEBUG_PAINT);
}
private static Path getTrianglePath(Point p1, int width) {
Point p2 = new Point(p1.x + width, p1.y);
Point p3 = new Point(p1.x, p1.y + width);
Path path = new Path();
path.moveTo(p1.x, p1.y);
path.lineTo(p2.x, p2.y);
path.lineTo(p3.x, p3.y);
return path;
}
}

View File

@ -0,0 +1,81 @@
/*
* 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.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.telephony.TelephonyManager;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* The default {@link java.util.concurrent.ExecutorService} used for new {@link Picasso} instances.
* <p/>
* Exists as a custom type so that we can differentiate the use of defaults versus a user-supplied
* instance.
*/
class PicassoExecutorService extends ThreadPoolExecutor {
private static final int DEFAULT_THREAD_COUNT = 3;
PicassoExecutorService() {
super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());
}
void adjustThreadCount(NetworkInfo info) {
if (info == null || !info.isConnectedOrConnecting()) {
setThreadCount(DEFAULT_THREAD_COUNT);
return;
}
switch (info.getType()) {
case ConnectivityManager.TYPE_WIFI:
case ConnectivityManager.TYPE_WIMAX:
case ConnectivityManager.TYPE_ETHERNET:
setThreadCount(4);
break;
case ConnectivityManager.TYPE_MOBILE:
switch (info.getSubtype()) {
case TelephonyManager.NETWORK_TYPE_LTE: // 4G
case TelephonyManager.NETWORK_TYPE_HSPAP:
case TelephonyManager.NETWORK_TYPE_EHRPD:
setThreadCount(3);
break;
case TelephonyManager.NETWORK_TYPE_UMTS: // 3G
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_EVDO_B:
setThreadCount(2);
break;
case TelephonyManager.NETWORK_TYPE_GPRS: // 2G
case TelephonyManager.NETWORK_TYPE_EDGE:
setThreadCount(1);
break;
default:
setThreadCount(DEFAULT_THREAD_COUNT);
}
break;
default:
setThreadCount(DEFAULT_THREAD_COUNT);
}
}
private void setThreadCount(int threadCount) {
setCorePoolSize(threadCount);
setMaximumPoolSize(threadCount);
}
}

View File

@ -0,0 +1,307 @@
/*
* 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.net.Uri;
import java.util.ArrayList;
import java.util.List;
import static java.util.Collections.unmodifiableList;
/** Immutable data about an image and the transformations that will be applied to it. */
public final class Request {
/**
* The image URI.
* <p>
* This is mutually exclusive with {@link #resourceId}.
*/
public final Uri uri;
/**
* The image resource ID.
* <p>
* This is mutually exclusive with {@link #uri}.
*/
public final int resourceId;
/** List of custom transformations to be applied after the built-in transformations. */
public final List<Transformation> transformations;
/** Target image width for resizing. */
public final int targetWidth;
/** Target image height for resizing. */
public final int targetHeight;
/**
* True if the final image should use the 'centerCrop' scale technique.
* <p>
* This is mutually exclusive with {@link #centerInside}.
*/
public final boolean centerCrop;
/**
* True if the final image should use the 'centerInside' scale technique.
* <p>
* This is mutually exclusive with {@link #centerCrop}.
*/
public final boolean centerInside;
/** Amount to rotate the image in degrees. */
public final float rotationDegrees;
/** Rotation pivot on the X axis. */
public final float rotationPivotX;
/** Rotation pivot on the Y axis. */
public final float rotationPivotY;
/** Whether or not {@link #rotationPivotX} and {@link #rotationPivotY} are set. */
public final boolean hasRotationPivot;
private Request(Uri uri, int resourceId, List<Transformation> transformations, int targetWidth,
int targetHeight, boolean centerCrop, boolean centerInside, float rotationDegrees,
float rotationPivotX, float rotationPivotY, boolean hasRotationPivot) {
this.uri = uri;
this.resourceId = resourceId;
if (transformations == null) {
this.transformations = null;
} else {
this.transformations = unmodifiableList(transformations);
}
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
this.centerCrop = centerCrop;
this.centerInside = centerInside;
this.rotationDegrees = rotationDegrees;
this.rotationPivotX = rotationPivotX;
this.rotationPivotY = rotationPivotY;
this.hasRotationPivot = hasRotationPivot;
}
String getName() {
if (uri != null) {
return uri.getPath();
}
return Integer.toHexString(resourceId);
}
public boolean hasSize() {
return targetWidth != 0;
}
boolean needsTransformation() {
return needsMatrixTransform() || hasCustomTransformations();
}
boolean needsMatrixTransform() {
return targetWidth != 0 || rotationDegrees != 0;
}
boolean hasCustomTransformations() {
return transformations != null;
}
public Builder buildUpon() {
return new Builder(this);
}
/** Builder for creating {@link Request} instances. */
public static final class Builder {
private Uri uri;
private int resourceId;
private int targetWidth;
private int targetHeight;
private boolean centerCrop;
private boolean centerInside;
private float rotationDegrees;
private float rotationPivotX;
private float rotationPivotY;
private boolean hasRotationPivot;
private List<Transformation> transformations;
/** Start building a request using the specified {@link Uri}. */
public Builder(Uri uri) {
setUri(uri);
}
/** Start building a request using the specified resource ID. */
public Builder(int resourceId) {
setResourceId(resourceId);
}
Builder(Uri uri, int resourceId) {
this.uri = uri;
this.resourceId = resourceId;
}
private Builder(Request request) {
uri = request.uri;
resourceId = request.resourceId;
targetWidth = request.targetWidth;
targetHeight = request.targetHeight;
centerCrop = request.centerCrop;
centerInside = request.centerInside;
rotationDegrees = request.rotationDegrees;
rotationPivotX = request.rotationPivotX;
rotationPivotY = request.rotationPivotY;
hasRotationPivot = request.hasRotationPivot;
if (request.transformations != null) {
transformations = new ArrayList<Transformation>(request.transformations);
}
}
boolean hasImage() {
return uri != null || resourceId != 0;
}
boolean hasSize() {
return targetWidth != 0;
}
/**
* Set the target image Uri.
* <p>
* This will clear an image resource ID if one is set.
*/
public Builder setUri(Uri uri) {
if (uri == null) {
throw new IllegalArgumentException("Image URI may not be null.");
}
this.uri = uri;
this.resourceId = 0;
return this;
}
/**
* Set the target image resource ID.
* <p>
* This will clear an image Uri if one is set.
*/
public Builder setResourceId(int resourceId) {
if (resourceId == 0) {
throw new IllegalArgumentException("Image resource ID may not be 0.");
}
this.resourceId = resourceId;
this.uri = null;
return this;
}
/** Resize the image to the specified size in pixels. */
public Builder resize(int targetWidth, int targetHeight) {
if (targetWidth <= 0) {
throw new IllegalArgumentException("Width must be positive number.");
}
if (targetHeight <= 0) {
throw new IllegalArgumentException("Height must be positive number.");
}
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
return this;
}
/** Clear the resize transformation, if any. This will also clear center crop/inside if set. */
public Builder clearResize() {
targetWidth = 0;
targetHeight = 0;
centerCrop = false;
centerInside = false;
return this;
}
/**
* Crops an image inside of the bounds specified by {@link #resize(int, int)} rather than
* distorting the aspect ratio. This cropping technique scales the image so that it fills the
* requested bounds and then crops the extra.
*/
public Builder centerCrop() {
if (centerInside) {
throw new IllegalStateException("Center crop can not be used after calling centerInside");
}
centerCrop = true;
return this;
}
/** Clear the center crop transformation flag, if set. */
public Builder clearCenterCrop() {
centerCrop = false;
return this;
}
/**
* Centers an image inside of the bounds specified by {@link #resize(int, int)}. This scales
* the image so that both dimensions are equal to or less than the requested bounds.
*/
public Builder centerInside() {
if (centerCrop) {
throw new IllegalStateException("Center inside can not be used after calling centerCrop");
}
centerInside = true;
return this;
}
/** Clear the center inside transformation flag, if set. */
public Builder clearCenterInside() {
centerInside = false;
return this;
}
/** Rotate the image by the specified degrees. */
public Builder rotate(float degrees) {
rotationDegrees = degrees;
return this;
}
/** Rotate the image by the specified degrees around a pivot point. */
public Builder rotate(float degrees, float pivotX, float pivotY) {
rotationDegrees = degrees;
rotationPivotX = pivotX;
rotationPivotY = pivotY;
hasRotationPivot = true;
return this;
}
/** Clear the rotation transformation, if any. */
public Builder clearRotation() {
rotationDegrees = 0;
rotationPivotX = 0;
rotationPivotY = 0;
hasRotationPivot = false;
return this;
}
/**
* Add a custom transformation to be applied to the image.
* <p/>
* Custom transformations will always be run after the built-in transformations.
*/
public Builder transform(Transformation transformation) {
if (transformation == null) {
throw new IllegalArgumentException("Transformation must not be null.");
}
if (transformations == null) {
transformations = new ArrayList<Transformation>(2);
}
transformations.add(transformation);
return this;
}
/** Create the immutable {@link Request} object. */
public Request build() {
if (centerInside && centerCrop) {
throw new IllegalStateException("Center crop and center inside can not be used together.");
}
if (centerCrop && targetWidth == 0) {
throw new IllegalStateException("Center crop requires calling resize.");
}
if (centerInside && targetWidth == 0) {
throw new IllegalStateException("Center inside requires calling resize.");
}
return new Request(uri, resourceId, transformations, targetWidth, targetHeight, centerCrop,
centerInside, rotationDegrees, rotationPivotX, rotationPivotY, hasRotationPivot);
}
}
}

View File

@ -0,0 +1,374 @@
/*
* 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.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.widget.ImageView;
import java.io.IOException;
import static com.squareup.picasso.BitmapHunter.forRequest;
import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;
import static com.squareup.picasso.Utils.checkNotMain;
import static com.squareup.picasso.Utils.createKey;
/** Fluent API for building an image download request. */
@SuppressWarnings("UnusedDeclaration") // Public API.
public class RequestCreator {
private final Picasso picasso;
private final Request.Builder data;
private boolean skipMemoryCache;
private boolean noFade;
private boolean deferred;
private int placeholderResId;
private Drawable placeholderDrawable;
private int errorResId;
private Drawable errorDrawable;
RequestCreator(Picasso picasso, Uri uri, int resourceId) {
if (picasso.shutdown) {
throw new IllegalStateException(
"Picasso instance already shut down. Cannot submit new requests.");
}
this.picasso = picasso;
this.data = new Request.Builder(uri, resourceId);
}
/**
* A placeholder drawable to be used while the image is being loaded. If the requested image is
* not immediately available in the memory cache then this resource will be set on the target
* {@link ImageView}.
*/
public RequestCreator placeholder(int placeholderResId) {
if (placeholderResId == 0) {
throw new IllegalArgumentException("Placeholder image resource invalid.");
}
if (placeholderDrawable != null) {
throw new IllegalStateException("Placeholder image already set.");
}
this.placeholderResId = placeholderResId;
return this;
}
/**
* A placeholder drawable to be used while the image is being loaded. If the requested image is
* not immediately available in the memory cache then this resource will be set on the target
* {@link ImageView}.
* <p>
* If you are not using a placeholder image but want to clear an existing image (such as when
* used in an {@link android.widget.Adapter adapter}), pass in {@code null}.
*/
public RequestCreator placeholder(Drawable placeholderDrawable) {
if (placeholderResId != 0) {
throw new IllegalStateException("Placeholder image already set.");
}
this.placeholderDrawable = placeholderDrawable;
return this;
}
/** An error drawable to be used if the request image could not be loaded. */
public RequestCreator error(int errorResId) {
if (errorResId == 0) {
throw new IllegalArgumentException("Error image resource invalid.");
}
if (errorDrawable != null) {
throw new IllegalStateException("Error image already set.");
}
this.errorResId = errorResId;
return this;
}
/** An error drawable to be used if the request image could not be loaded. */
public RequestCreator error(Drawable errorDrawable) {
if (errorDrawable == null) {
throw new IllegalArgumentException("Error image may not be null.");
}
if (errorResId != 0) {
throw new IllegalStateException("Error image already set.");
}
this.errorDrawable = errorDrawable;
return this;
}
/**
* Attempt to resize the image to fit exactly into the target {@link ImageView}'s bounds. This
* will result in delayed execution of the request until the {@link ImageView} has been measured.
* <p/>
* <em>Note:</em> This method works only when your target is an {@link ImageView}.
*/
public RequestCreator fit() {
deferred = true;
return this;
}
/** Internal use only. Used by {@link DeferredRequestCreator}. */
RequestCreator unfit() {
deferred = false;
return this;
}
/** Resize the image to the specified dimension size. */
public RequestCreator resizeDimen(int targetWidthResId, int targetHeightResId) {
Resources resources = picasso.context.getResources();
int targetWidth = resources.getDimensionPixelSize(targetWidthResId);
int targetHeight = resources.getDimensionPixelSize(targetHeightResId);
return resize(targetWidth, targetHeight);
}
/** Resize the image to the specified size in pixels. */
public RequestCreator resize(int targetWidth, int targetHeight) {
data.resize(targetWidth, targetHeight);
return this;
}
/**
* Crops an image inside of the bounds specified by {@link #resize(int, int)} rather than
* distorting the aspect ratio. This cropping technique scales the image so that it fills the
* requested bounds and then crops the extra.
*/
public RequestCreator centerCrop() {
data.centerCrop();
return this;
}
/**
* Centers an image inside of the bounds specified by {@link #resize(int, int)}. This scales
* the image so that both dimensions are equal to or less than the requested bounds.
*/
public RequestCreator centerInside() {
data.centerInside();
return this;
}
/** Rotate the image by the specified degrees. */
public RequestCreator rotate(float degrees) {
data.rotate(degrees);
return this;
}
/** Rotate the image by the specified degrees around a pivot point. */
public RequestCreator rotate(float degrees, float pivotX, float pivotY) {
data.rotate(degrees, pivotX, pivotY);
return this;
}
/**
* Add a custom transformation to be applied to the image.
* <p/>
* Custom transformations will always be run after the built-in transformations.
*/
// TODO show example of calling resize after a transform in the javadoc
public RequestCreator transform(Transformation transformation) {
data.transform(transformation);
return this;
}
/**
* Indicate that this action should not use the memory cache for attempting to load or save the
* image. This can be useful when you know an image will only ever be used once (e.g., loading
* an image from the filesystem and uploading to a remote server).
*/
public RequestCreator skipMemoryCache() {
skipMemoryCache = true;
return this;
}
/** Disable brief fade in of images loaded from the disk cache or network. */
public RequestCreator noFade() {
noFade = true;
return this;
}
/** Synchronously fulfill this request. Must not be called from the main thread. */
public Bitmap get() throws IOException {
checkNotMain();
if (deferred) {
throw new IllegalStateException("Fit cannot be used with get.");
}
if (!data.hasImage()) {
return null;
}
Request finalData = picasso.transformRequest(data.build());
String key = createKey(finalData);
Action action = new GetAction(picasso, finalData, skipMemoryCache, key);
return forRequest(picasso.context, picasso, picasso.dispatcher, picasso.cache, picasso.stats,
action, picasso.dispatcher.downloader).hunt();
}
/**
* Asynchronously fulfills the request without a {@link ImageView} or {@link Target}. This is
* useful when you want to warm up the cache with an image.
*/
public void fetch() {
if (deferred) {
throw new IllegalStateException("Fit cannot be used with fetch.");
}
if (data.hasImage()) {
Request finalData = picasso.transformRequest(data.build());
String key = createKey(finalData);
Action action = new FetchAction(picasso, finalData, skipMemoryCache, key);
picasso.enqueueAndSubmit(action);
}
}
/**
* Asynchronously fulfills the request into the specified {@link Target}. In most cases, you
* should use this when you are dealing with a custom {@link android.view.View View} or view
* holder which should implement the {@link Target} interface.
* <p>
* Implementing on a {@link android.view.View View}:
* <blockquote><pre>
* public class ProfileView extends FrameLayout implements Target {
* {@literal @}Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
* setBackgroundDrawable(new BitmapDrawable(bitmap));
* }
*
* {@literal @}Override public void onBitmapFailed() {
* setBackgroundResource(R.drawable.profile_error);
* }
* }
* </pre></blockquote>
* Implementing on a view holder object for use inside of an adapter:
* <blockquote><pre>
* public class ViewHolder implements Target {
* public FrameLayout frame;
* public TextView name;
*
* {@literal @}Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
* frame.setBackgroundDrawable(new BitmapDrawable(bitmap));
* }
*
* {@literal @}Override public void onBitmapFailed() {
* frame.setBackgroundResource(R.drawable.profile_error);
* }
* }
* </pre></blockquote>
* <p>
* <em>Note:</em> This method keeps a weak reference to the {@link Target} instance and will be
* garbage collected if you do not keep a strong reference to it. To receive callbacks when an
* image is loaded use {@link #into(android.widget.ImageView, Callback)}.
*/
public void into(Target target) {
if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
}
if (deferred) {
throw new IllegalStateException("Fit cannot be used with a Target.");
}
Drawable drawable =
placeholderResId != 0 ? picasso.context.getResources().getDrawable(placeholderResId)
: placeholderDrawable;
if (!data.hasImage()) {
picasso.cancelRequest(target);
target.onPrepareLoad(drawable);
return;
}
Request finalData = picasso.transformRequest(data.build());
String requestKey = createKey(finalData);
if (!skipMemoryCache) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
target.onBitmapLoaded(bitmap, MEMORY);
return;
}
}
target.onPrepareLoad(drawable);
Action action = new TargetAction(picasso, target, finalData, skipMemoryCache, requestKey);
picasso.enqueueAndSubmit(action);
}
/**
* Asynchronously fulfills the request into the specified {@link ImageView}.
* <p/>
* <em>Note:</em> This method keeps a weak reference to the {@link ImageView} instance and will
* automatically support object recycling.
*/
public void into(ImageView target) {
into(target, null);
}
/**
* Asynchronously fulfills the request into the specified {@link ImageView} and invokes the
* target {@link Callback} if it's not {@code null}.
* <p/>
* <em>Note:</em> The {@link Callback} param is a strong reference and will prevent your
* {@link android.app.Activity} or {@link android.app.Fragment} from being garbage collected. If
* you use this method, it is <b>strongly</b> recommended you invoke an adjacent
* {@link Picasso#cancelRequest(android.widget.ImageView)} call to prevent temporary leaking.
*/
public void into(ImageView target, Callback callback) {
if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
}
if (!data.hasImage()) {
picasso.cancelRequest(target);
PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
return;
}
if (deferred) {
if (data.hasSize()) {
throw new IllegalStateException("Fit cannot be used with resize.");
}
int measuredWidth = target.getMeasuredWidth();
int measuredHeight = target.getMeasuredHeight();
if (measuredWidth == 0 || measuredHeight == 0) {
PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
picasso.defer(target, new DeferredRequestCreator(this, target, callback));
return;
}
data.resize(measuredWidth, measuredHeight);
}
Request finalData = picasso.transformRequest(data.build());
String requestKey = createKey(finalData);
if (!skipMemoryCache) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
PicassoDrawable.setBitmap(target, picasso.context, bitmap, MEMORY, noFade,
picasso.debugging);
if (callback != null) {
callback.onSuccess();
}
return;
}
}
PicassoDrawable.setPlaceholder(target, placeholderResId, placeholderDrawable);
Action action =
new ImageViewAction(picasso, target, finalData, skipMemoryCache, noFade, errorResId,
errorDrawable, requestKey, callback);
picasso.enqueueAndSubmit(action);
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.IOException;
import static com.squareup.picasso.Picasso.LoadedFrom.DISK;
class ResourceBitmapHunter extends BitmapHunter {
private final Context context;
ResourceBitmapHunter(Context context, Picasso picasso, Dispatcher dispatcher, Cache cache,
Stats stats, Action action) {
super(picasso, dispatcher, cache, stats, action);
this.context = context;
}
@Override Bitmap decode(Request data) throws IOException {
Resources res = Utils.getResources(context, data);
int id = Utils.getResourceId(res, data);
return decodeResource(res, id, data);
}
@Override Picasso.LoadedFrom getLoadedFrom() {
return DISK;
}
private Bitmap decodeResource(Resources resources, int id, Request data) {
BitmapFactory.Options bitmapOptions = null;
if (data.hasSize()) {
bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, id, bitmapOptions);
calculateInSampleSize(data.targetWidth, data.targetHeight, bitmapOptions);
}
return BitmapFactory.decodeResource(resources, id, bitmapOptions);
}
}

View File

@ -0,0 +1,143 @@
/*
* 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.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
class Stats {
private static final int CACHE_HIT = 0;
private static final int CACHE_MISS = 1;
private static final int BITMAP_DECODE_FINISHED = 2;
private static final int BITMAP_TRANSFORMED_FINISHED = 3;
private static final String STATS_THREAD_NAME = Utils.THREAD_PREFIX + "Stats";
final HandlerThread statsThread;
final Cache cache;
final Handler handler;
long cacheHits;
long cacheMisses;
long totalOriginalBitmapSize;
long totalTransformedBitmapSize;
long averageOriginalBitmapSize;
long averageTransformedBitmapSize;
int originalBitmapCount;
int transformedBitmapCount;
Stats(Cache cache) {
this.cache = cache;
this.statsThread = new HandlerThread(STATS_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
this.statsThread.start();
this.handler = new StatsHandler(statsThread.getLooper(), this);
}
void dispatchBitmapDecoded(Bitmap bitmap) {
processBitmap(bitmap, BITMAP_DECODE_FINISHED);
}
void dispatchBitmapTransformed(Bitmap bitmap) {
processBitmap(bitmap, BITMAP_TRANSFORMED_FINISHED);
}
void dispatchCacheHit() {
handler.sendEmptyMessage(CACHE_HIT);
}
void dispatchCacheMiss() {
handler.sendEmptyMessage(CACHE_MISS);
}
void shutdown() {
statsThread.quit();
}
void performCacheHit() {
cacheHits++;
}
void performCacheMiss() {
cacheMisses++;
}
void performBitmapDecoded(long size) {
originalBitmapCount++;
totalOriginalBitmapSize += size;
averageOriginalBitmapSize = getAverage(originalBitmapCount, totalOriginalBitmapSize);
}
void performBitmapTransformed(long size) {
transformedBitmapCount++;
totalTransformedBitmapSize += size;
averageTransformedBitmapSize = getAverage(originalBitmapCount, totalTransformedBitmapSize);
}
synchronized StatsSnapshot createSnapshot() {
return new StatsSnapshot(cache.maxSize(), cache.size(), cacheHits, cacheMisses,
totalOriginalBitmapSize, totalTransformedBitmapSize, averageOriginalBitmapSize,
averageTransformedBitmapSize, originalBitmapCount, transformedBitmapCount,
System.currentTimeMillis());
}
private void processBitmap(Bitmap bitmap, int what) {
// Never send bitmaps to the handler as they could be recycled before we process them.
int bitmapSize = Utils.getBitmapBytes(bitmap);
handler.sendMessage(handler.obtainMessage(what, bitmapSize, 0));
}
private static long getAverage(int count, long totalSize) {
return totalSize / count;
}
private static class StatsHandler extends Handler {
private final Stats stats;
public StatsHandler(Looper looper, Stats stats) {
super(looper);
this.stats = stats;
}
@Override public void handleMessage(final Message msg) {
switch (msg.what) {
case CACHE_HIT:
stats.performCacheHit();
break;
case CACHE_MISS:
stats.performCacheMiss();
break;
case BITMAP_DECODE_FINISHED:
stats.performBitmapDecoded(msg.arg1);
break;
case BITMAP_TRANSFORMED_FINISHED:
stats.performBitmapTransformed(msg.arg1);
break;
default:
Picasso.HANDLER.post(new Runnable() {
@Override public void run() {
throw new AssertionError("Unhandled stats message." + msg.what);
}
});
}
}
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.util.Log;
import java.io.PrintWriter;
import java.io.StringWriter;
/** Represents all stats for a {@link Picasso} instance at a single point in time. */
public class StatsSnapshot {
private static final String TAG = "Picasso";
public final int maxSize;
public final int size;
public final long cacheHits;
public final long cacheMisses;
public final long totalOriginalBitmapSize;
public final long totalTransformedBitmapSize;
public final long averageOriginalBitmapSize;
public final long averageTransformedBitmapSize;
public final int originalBitmapCount;
public final int transformedBitmapCount;
public final long timeStamp;
public StatsSnapshot(int maxSize, int size, long cacheHits, long cacheMisses,
long totalOriginalBitmapSize, long totalTransformedBitmapSize, long averageOriginalBitmapSize,
long averageTransformedBitmapSize, int originalBitmapCount, int transformedBitmapCount,
long timeStamp) {
this.maxSize = maxSize;
this.size = size;
this.cacheHits = cacheHits;
this.cacheMisses = cacheMisses;
this.totalOriginalBitmapSize = totalOriginalBitmapSize;
this.totalTransformedBitmapSize = totalTransformedBitmapSize;
this.averageOriginalBitmapSize = averageOriginalBitmapSize;
this.averageTransformedBitmapSize = averageTransformedBitmapSize;
this.originalBitmapCount = originalBitmapCount;
this.transformedBitmapCount = transformedBitmapCount;
this.timeStamp = timeStamp;
}
/** Prints out this {@link StatsSnapshot} into log. */
public void dump() {
StringWriter logWriter = new StringWriter();
dump(new PrintWriter(logWriter));
Log.i(TAG, logWriter.toString());
}
/** Prints out this {@link StatsSnapshot} with the the provided {@link PrintWriter}. */
public void dump(PrintWriter writer) {
writer.println("===============BEGIN PICASSO STATS ===============");
writer.println("Memory Cache Stats");
writer.print(" Max Cache Size: ");
writer.println(maxSize);
writer.print(" Cache Size: ");
writer.println(size);
writer.print(" Cache % Full: ");
writer.println((int) Math.ceil((float) size / maxSize * 100));
writer.print(" Cache Hits: ");
writer.println(cacheHits);
writer.print(" Cache Misses: ");
writer.println(cacheMisses);
writer.println("Bitmap Stats");
writer.print(" Total Bitmaps Decoded: ");
writer.println(originalBitmapCount);
writer.print(" Total Bitmap Size: ");
writer.println(totalOriginalBitmapSize);
writer.print(" Total Transformed Bitmaps: ");
writer.println(transformedBitmapCount);
writer.print(" Total Transformed Bitmap Size: ");
writer.println(totalTransformedBitmapSize);
writer.print(" Average Bitmap Size: ");
writer.println(averageOriginalBitmapSize);
writer.print(" Average Transformed Bitmap Size: ");
writer.println(averageTransformedBitmapSize);
writer.println("===============END PICASSO STATS ===============");
writer.flush();
}
@Override public String toString() {
return "StatsSnapshot{"
+ "maxSize="
+ maxSize
+ ", size="
+ size
+ ", cacheHits="
+ cacheHits
+ ", cacheMisses="
+ cacheMisses
+ ", totalOriginalBitmapSize="
+ totalOriginalBitmapSize
+ ", totalTransformedBitmapSize="
+ totalTransformedBitmapSize
+ ", averageOriginalBitmapSize="
+ averageOriginalBitmapSize
+ ", averageTransformedBitmapSize="
+ averageTransformedBitmapSize
+ ", originalBitmapCount="
+ originalBitmapCount
+ ", transformedBitmapCount="
+ transformedBitmapCount
+ ", timeStamp="
+ timeStamp
+ '}';
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.drawable.Drawable;
import static com.squareup.picasso.Picasso.LoadedFrom;
/**
* Represents an arbitrary listener for image loading.
* <p/>
* Objects implementing this class <strong>must</strong> have a working implementation of
* {@link #equals(Object)} and {@link #hashCode()} for proper storage internally. Instances of this
* interface will also be compared to determine if view recycling is occurring. It is recommended
* that you add this interface directly on to a custom view type when using in an adapter to ensure
* correct recycling behavior.
*/
public interface Target {
/**
* Callback when an image has been successfully loaded.
* <p/>
* <strong>Note:</strong> You must not recycle the bitmap.
*/
void onBitmapLoaded(Bitmap bitmap, LoadedFrom from);
/** Callback indicating the image could not be successfully loaded. */
void onBitmapFailed(Drawable errorDrawable);
/** Callback invoked right before your request is submitted. */
void onPrepareLoad(Drawable placeHolderDrawable);
}

View File

@ -0,0 +1,46 @@
/*
* 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;
final class TargetAction extends Action<Target> {
TargetAction(Picasso picasso, Target target, Request data, boolean skipCache, String key) {
super(picasso, target, data, skipCache, false, 0, null, key);
}
@Override void complete(Bitmap result, Picasso.LoadedFrom from) {
if (result == null) {
throw new AssertionError(
String.format("Attempted to complete action with no result!\n%s", this));
}
Target target = getTarget();
if (target != null) {
target.onBitmapLoaded(result, from);
if (result.isRecycled()) {
throw new IllegalStateException("Target callback must not recycle bitmap!");
}
}
}
@Override void error() {
Target target = getTarget();
if (target != null) {
target.onBitmapFailed(errorDrawable);
}
}
}

View File

@ -0,0 +1,34 @@
/*
* 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;
/** Image transformation. */
public interface Transformation {
/**
* Transform the source bitmap into a new bitmap. If you create a new bitmap instance, you must
* call {@link android.graphics.Bitmap#recycle()} on {@code source}. You may return the original
* if no transformation is required.
*/
Bitmap transform(Bitmap source);
/**
* Returns a unique key for the transformation, used for caching purposes. If the transformation
* has parameters (e.g. size, scale factor, etc) then these should be part of the key.
*/
String key();
}

View File

@ -0,0 +1,100 @@
/*
* 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.content.Context;
import android.net.Uri;
import android.net.http.HttpResponseCache;
import android.os.Build;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import static com.squareup.picasso.Utils.parseResponseSourceHeader;
/**
* A {@link Downloader} which uses {@link HttpURLConnection} to download images. A disk cache of 2%
* of the total available space will be used (capped at 50MB) will automatically be installed in the
* application's cache directory, when available.
*/
public class UrlConnectionDownloader implements Downloader {
static final String RESPONSE_SOURCE = "X-Android-Response-Source";
private static final Object lock = new Object();
static volatile Object cache;
private final Context context;
public UrlConnectionDownloader(Context context) {
this.context = context.getApplicationContext();
}
protected HttpURLConnection openConnection(Uri path) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(path.toString()).openConnection();
connection.setConnectTimeout(Utils.DEFAULT_CONNECT_TIMEOUT);
connection.setReadTimeout(Utils.DEFAULT_READ_TIMEOUT);
return connection;
}
@Override public Response load(Uri uri, boolean localCacheOnly) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
installCacheIfNeeded(context);
}
HttpURLConnection connection = openConnection(uri);
connection.setUseCaches(true);
if (localCacheOnly) {
connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
}
int responseCode = connection.getResponseCode();
if (responseCode >= 300) {
connection.disconnect();
throw new ResponseException(responseCode + " " + connection.getResponseMessage());
}
boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));
return new Response(connection.getInputStream(), fromCache);
}
private static void installCacheIfNeeded(Context context) {
// DCL + volatile should be safe after Java 5.
if (cache == null) {
try {
synchronized (lock) {
if (cache == null) {
cache = ResponseCacheIcs.install(context);
}
}
} catch (IOException ignored) {
}
}
}
private static class ResponseCacheIcs {
static Object install(Context context) throws IOException {
File cacheDir = Utils.createDefaultCacheDir(context);
HttpResponseCache cache = HttpResponseCache.getInstalled();
if (cache == null) {
long maxSize = Utils.calculateDiskCacheSize(cacheDir);
cache = HttpResponseCache.install(cacheDir, maxSize);
}
return cache;
}
}
}

View File

@ -0,0 +1,304 @@
/*
* 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.TargetApi;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Looper;
import android.os.Process;
import android.os.StatFs;
import android.provider.Settings;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.concurrent.ThreadFactory;
import static android.content.Context.ACTIVITY_SERVICE;
import static android.content.pm.ApplicationInfo.FLAG_LARGE_HEAP;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.HONEYCOMB;
import static android.os.Build.VERSION_CODES.HONEYCOMB_MR1;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static android.provider.Settings.System.AIRPLANE_MODE_ON;
final class Utils {
static final String THREAD_PREFIX = "Picasso-";
static final String THREAD_IDLE_NAME = THREAD_PREFIX + "Idle";
static final int DEFAULT_READ_TIMEOUT = 20 * 1000; // 20s
static final int DEFAULT_CONNECT_TIMEOUT = 15 * 1000; // 15s
private static final String PICASSO_CACHE = "picasso-cache";
private static final int KEY_PADDING = 50; // Determined by exact science.
private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
/* WebP file header
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 'R' | 'I' | 'F' | 'F' |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| File Size |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 'W' | 'E' | 'B' | 'P' |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
private static final int WEBP_FILE_HEADER_SIZE = 12;
private static final String WEBP_FILE_HEADER_RIFF = "RIFF";
private static final String WEBP_FILE_HEADER_WEBP = "WEBP";
private Utils() {
// No instances.
}
static int getBitmapBytes(Bitmap bitmap) {
int result;
if (SDK_INT >= HONEYCOMB_MR1) {
result = BitmapHoneycombMR1.getByteCount(bitmap);
} else {
result = bitmap.getRowBytes() * bitmap.getHeight();
}
if (result < 0) {
throw new IllegalStateException("Negative size: " + bitmap);
}
return result;
}
static void checkNotMain() {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
throw new IllegalStateException("Method call should not happen from the main thread.");
}
}
static String createKey(Request data) {
StringBuilder builder;
if (data.uri != null) {
String path = data.uri.toString();
builder = new StringBuilder(path.length() + KEY_PADDING);
builder.append(path);
} else {
builder = new StringBuilder(KEY_PADDING);
builder.append(data.resourceId);
}
builder.append('\n');
if (data.rotationDegrees != 0) {
builder.append("rotation:").append(data.rotationDegrees);
if (data.hasRotationPivot) {
builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);
}
builder.append('\n');
}
if (data.targetWidth != 0) {
builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight);
builder.append('\n');
}
if (data.centerCrop) {
builder.append("centerCrop\n");
} else if (data.centerInside) {
builder.append("centerInside\n");
}
if (data.transformations != null) {
//noinspection ForLoopReplaceableByForEach
for (int i = 0, count = data.transformations.size(); i < count; i++) {
builder.append(data.transformations.get(i).key());
builder.append('\n');
}
}
return builder.toString();
}
static void closeQuietly(InputStream is) {
if (is == null) return;
try {
is.close();
} catch (IOException ignored) {
}
}
/** Returns {@code true} if header indicates the response body was loaded from the disk cache. */
static boolean parseResponseSourceHeader(String header) {
if (header == null) {
return false;
}
String[] parts = header.split(" ", 2);
if ("CACHE".equals(parts[0])) {
return true;
}
if (parts.length == 1) {
return false;
}
try {
return "CONDITIONAL_CACHE".equals(parts[0]) && Integer.parseInt(parts[1]) == 304;
} catch (NumberFormatException e) {
return false;
}
}
static Downloader createDefaultDownloader(Context context) {
return new UrlConnectionDownloader(context);
}
static File createDefaultCacheDir(Context context) {
File cache = new File(context.getApplicationContext().getCacheDir(), PICASSO_CACHE);
if (!cache.exists()) {
cache.mkdirs();
}
return cache;
}
static long calculateDiskCacheSize(File dir) {
long size = MIN_DISK_CACHE_SIZE;
try {
StatFs statFs = new StatFs(dir.getAbsolutePath());
long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize();
// Target 2% of the total space.
size = available / 50;
} catch (IllegalArgumentException ignored) {
}
// Bound inside min/max size for disk cache.
return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
}
static int calculateMemoryCacheSize(Context context) {
ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;
int memoryClass = am.getMemoryClass();
if (largeHeap && SDK_INT >= HONEYCOMB) {
memoryClass = ActivityManagerHoneycomb.getLargeMemoryClass(am);
}
// Target ~15% of the available heap.
return 1024 * 1024 * memoryClass / 7;
}
static boolean isAirplaneModeOn(Context context) {
ContentResolver contentResolver = context.getContentResolver();
return Settings.System.getInt(contentResolver, AIRPLANE_MODE_ON, 0) != 0;
}
static boolean hasPermission(Context context, String permission) {
return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}
static byte[] toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 4];
int n = 0;
while (-1 != (n = input.read(buffer))) {
byteArrayOutputStream.write(buffer, 0, n);
}
return byteArrayOutputStream.toByteArray();
}
static boolean isWebPFile(InputStream stream) throws IOException {
byte[] fileHeaderBytes = new byte[WEBP_FILE_HEADER_SIZE];
boolean isWebPFile = false;
if (stream.read(fileHeaderBytes, 0, WEBP_FILE_HEADER_SIZE) == WEBP_FILE_HEADER_SIZE) {
// If a file's header starts with RIFF and end with WEBP, the file is a WebP file
isWebPFile = WEBP_FILE_HEADER_RIFF.equals(new String(fileHeaderBytes, 0, 4, "US-ASCII"))
&& WEBP_FILE_HEADER_WEBP.equals(new String(fileHeaderBytes, 8, 4, "US-ASCII"));
}
return isWebPFile;
}
static int getResourceId(Resources resources, Request data) throws FileNotFoundException {
if (data.resourceId != 0 || data.uri == null) {
return data.resourceId;
}
String pkg = data.uri.getAuthority();
if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri);
int id;
List<String> segments = data.uri.getPathSegments();
if (segments == null || segments.isEmpty()) {
throw new FileNotFoundException("No path segments: " + data.uri);
} else if (segments.size() == 1) {
try {
id = Integer.parseInt(segments.get(0));
} catch (NumberFormatException e) {
throw new FileNotFoundException("Last path segment is not a resource ID: " + data.uri);
}
} else if (segments.size() == 2) {
String type = segments.get(0);
String name = segments.get(1);
id = resources.getIdentifier(name, type, pkg);
} else {
throw new FileNotFoundException("More than two path segments: " + data.uri);
}
return id;
}
static Resources getResources(Context context, Request data) throws FileNotFoundException {
if (data.resourceId != 0 || data.uri == null) {
return context.getResources();
}
String pkg = data.uri.getAuthority();
if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri);
try {
PackageManager pm = context.getPackageManager();
return pm.getResourcesForApplication(pkg);
} catch (PackageManager.NameNotFoundException e) {
throw new FileNotFoundException("Unable to obtain resources for package: " + data.uri);
}
}
@TargetApi(HONEYCOMB)
private static class ActivityManagerHoneycomb {
static int getLargeMemoryClass(ActivityManager activityManager) {
return activityManager.getLargeMemoryClass();
}
}
static class PicassoThreadFactory implements ThreadFactory {
@SuppressWarnings("NullableProblems")
public Thread newThread(Runnable r) {
return new PicassoThread(r);
}
}
private static class PicassoThread extends Thread {
public PicassoThread(Runnable r) {
super(r);
}
@Override public void run() {
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
super.run();
}
}
@TargetApi(HONEYCOMB_MR1)
private static class BitmapHoneycombMR1 {
static int getByteCount(Bitmap bitmap) {
return bitmap.getByteCount();
}
}
}