diff options
Diffstat (limited to 'mobile/android/thirdparty/com/squareup')
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(); + } + } +} |