summaryrefslogtreecommitdiffstats
path: root/mobile/android/thirdparty/com/squareup
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/thirdparty/com/squareup')
-rw-r--r--mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java21
-rw-r--r--mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java20
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Action.java83
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java51
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java357
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Cache.java64
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Callback.java31
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java130
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java67
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java70
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java315
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Downloader.java99
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/FetchAction.java30
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java57
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/GetAction.java30
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java75
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/LruCache.java146
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java157
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java116
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java113
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Picasso.java522
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java186
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java81
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Request.java307
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java374
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java55
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Stats.java143
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java120
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Target.java45
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/TargetAction.java46
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Transformation.java34
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java100
-rw-r--r--mobile/android/thirdparty/com/squareup/picasso/Utils.java304
33 files changed, 4349 insertions, 0 deletions
diff --git a/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java b/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java
new file mode 100644
index 000000000..a9e44437d
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/leakcanary/LeakCanary.java
@@ -0,0 +1,21 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package com.squareup.leakcanary;
+
+import android.app.Application;
+
+/**
+ * A no-op version of {@link LeakCanary} that can be used in release builds.
+ */
+public final class LeakCanary {
+ public static RefWatcher install(Application application) {
+ return RefWatcher.DISABLED;
+ }
+
+ private LeakCanary() {
+ throw new AssertionError();
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java b/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java
new file mode 100644
index 000000000..3c75dcdee
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/leakcanary/RefWatcher.java
@@ -0,0 +1,20 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package com.squareup.leakcanary;
+
+/**
+ * No-op implementation of {@link RefWatcher} for release builds. Please use {@link
+ * RefWatcher#DISABLED}.
+ */
+public final class RefWatcher {
+ public static final RefWatcher DISABLED = new RefWatcher();
+
+ private RefWatcher() {}
+
+ public void watch(Object watchedReference) {}
+
+ public void watch(Object watchedReference, String referenceName) {}
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Action.java b/mobile/android/thirdparty/com/squareup/picasso/Action.java
new file mode 100644
index 000000000..5f176d770
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Action.java
@@ -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;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java
new file mode 100644
index 000000000..c0245ed3f
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/AssetBitmapHunter.java
@@ -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);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java
new file mode 100644
index 000000000..b8aa87c48
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/BitmapHunter.java
@@ -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;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Cache.java b/mobile/android/thirdparty/com/squareup/picasso/Cache.java
new file mode 100644
index 000000000..bce297f9e
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Cache.java
@@ -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() {
+ }
+ };
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Callback.java b/mobile/android/thirdparty/com/squareup/picasso/Callback.java
new file mode 100644
index 000000000..d93620889
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Callback.java
@@ -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() {
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java
new file mode 100644
index 000000000..9444167b4
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ContactsPhotoBitmapHunter.java
@@ -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);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java
new file mode 100644
index 000000000..624ffe078
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ContentStreamBitmapHunter.java
@@ -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);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java b/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java
new file mode 100644
index 000000000..fbdaab1c3
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/DeferredRequestCreator.java
@@ -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);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java b/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java
new file mode 100644
index 000000000..6401431fb
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Dispatcher.java
@@ -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());
+ }
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Downloader.java b/mobile/android/thirdparty/com/squareup/picasso/Downloader.java
new file mode 100644
index 000000000..33a909371
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Downloader.java
@@ -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;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java b/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java
new file mode 100644
index 000000000..d8f1c3fb4
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/FetchAction.java
@@ -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() {
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java
new file mode 100644
index 000000000..dac38fb80
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/FileBitmapHunter.java
@@ -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;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/GetAction.java b/mobile/android/thirdparty/com/squareup/picasso/GetAction.java
new file mode 100644
index 000000000..e30024e89
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/GetAction.java
@@ -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() {
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java b/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java
new file mode 100644
index 000000000..16f550907
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ImageViewAction.java
@@ -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;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/LruCache.java b/mobile/android/thirdparty/com/squareup/picasso/LruCache.java
new file mode 100644
index 000000000..5d5f07fcb
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/LruCache.java
@@ -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;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java b/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java
new file mode 100644
index 000000000..17043a1b0
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/MarkableInputStream.java
@@ -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();
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java
new file mode 100644
index 000000000..4f8ae1c24
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/MediaStoreBitmapHunter.java
@@ -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;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java
new file mode 100644
index 000000000..6d148211d
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/NetworkBitmapHunter.java
@@ -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);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Picasso.java b/mobile/android/thirdparty/com/squareup/picasso/Picasso.java
new file mode 100644
index 000000000..9b510f977
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Picasso.java
@@ -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;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java b/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java
new file mode 100644
index 000000000..07f762c31
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/PicassoDrawable.java
@@ -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;
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java b/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java
new file mode 100644
index 000000000..875dd2dda
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/PicassoExecutorService.java
@@ -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);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Request.java b/mobile/android/thirdparty/com/squareup/picasso/Request.java
new file mode 100644
index 000000000..8e9c32460
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Request.java
@@ -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);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java b/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java
new file mode 100644
index 000000000..3a5ca3a9f
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/RequestCreator.java
@@ -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);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java b/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java
new file mode 100644
index 000000000..fee76b200
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/ResourceBitmapHunter.java
@@ -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);
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Stats.java b/mobile/android/thirdparty/com/squareup/picasso/Stats.java
new file mode 100644
index 000000000..3eaac0249
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Stats.java
@@ -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);
+ }
+ });
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java b/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java
new file mode 100644
index 000000000..5f276ebf2
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/StatsSnapshot.java
@@ -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
+ + '}';
+ }
+} \ No newline at end of file
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Target.java b/mobile/android/thirdparty/com/squareup/picasso/Target.java
new file mode 100644
index 000000000..ad3ce6fcf
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Target.java
@@ -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);
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java b/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java
new file mode 100644
index 000000000..77a40f51d
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/TargetAction.java
@@ -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);
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Transformation.java b/mobile/android/thirdparty/com/squareup/picasso/Transformation.java
new file mode 100644
index 000000000..2c59f160c
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Transformation.java
@@ -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();
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java b/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java
new file mode 100644
index 000000000..50f9b2b98
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/UrlConnectionDownloader.java
@@ -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;
+ }
+ }
+}
diff --git a/mobile/android/thirdparty/com/squareup/picasso/Utils.java b/mobile/android/thirdparty/com/squareup/picasso/Utils.java
new file mode 100644
index 000000000..bafe93f98
--- /dev/null
+++ b/mobile/android/thirdparty/com/squareup/picasso/Utils.java
@@ -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();
+ }
+ }
+}