diff options
24 files changed, 3524 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..193a7c2 --- /dev/null +++ b/Android.mk @@ -0,0 +1,28 @@ +# Copyright (C) 2013 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE := android-opt-bitmap + +LOCAL_SDK_VERSION := 16 + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-logtags-files-under, src) + +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..467996f --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.bitmap" + android:versionCode="1" + android:versionName="1.0" > + + <uses-sdk + android:minSdkVersion="15" + android:targetSdkVersion="16" /> + +</manifest> diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 0000000..c01dc56 --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013 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. +--> +<resources> + <!-- Background color for images while they're loading --> + <color name="bitmap_placeholder_background_color">#fff1f1f2</color> +</resources> diff --git a/res/values/constants.xml b/res/values/constants.xml new file mode 100644 index 0000000..5545adb --- /dev/null +++ b/res/values/constants.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013 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. +--> +<resources> + <!-- Duration of fade in/out animation for images --> + <integer name="bitmap_fade_animation_duration">500</integer> + <!-- Delay before showing progress bar animations for images that are loading --> + <integer name="bitmap_progress_animation_delay">2000</integer> + <!-- Duration of placeholder pulse animation for images --> + <integer name="bitmap_placeholder_animation_duration">1000</integer> + <!-- Duration of progress bar animation for images --> + <integer name="bitmap_progress_animation_duration">4000</integer> +</resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml new file mode 100644 index 0000000..5b2a94b --- /dev/null +++ b/res/values/dimens.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013 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. +--> +<resources> + <!-- TODO: don't statically define these because the placeholder/progress can be anything. --> + <!-- Width and height of the placeholder drawable. This must correspond to ic_placeholder. --> + <dimen name="placeholder_size">20dp</dimen> + <!-- Width and height of the progress bar drawable. This must correspond to ic_progressbar. --> + <dimen name="progress_bar_size">28dp</dimen> +</resources> diff --git a/src/com/android/bitmap/BitmapCache.java b/src/com/android/bitmap/BitmapCache.java new file mode 100644 index 0000000..4695d72 --- /dev/null +++ b/src/com/android/bitmap/BitmapCache.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +public interface BitmapCache extends PooledCache<DecodeTask.Request, ReusableBitmap> { + + /** + * Notify the cache when it should start and stop blocking for cache misses. + * If {@link #setBlocking(true)} has been called and if the pool is empty, {@link #poll()} + * should block until the pool is repopulated, or until {@link #setBlocking(false)} is called. + */ + void setBlocking(boolean blocking); + +} diff --git a/src/com/android/bitmap/ContiguousFIFOAggregator.java b/src/com/android/bitmap/ContiguousFIFOAggregator.java new file mode 100644 index 0000000..9716fb8 --- /dev/null +++ b/src/com/android/bitmap/ContiguousFIFOAggregator.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +import android.util.Log; +import android.util.SparseArray; + +import com.android.bitmap.util.Trace; + +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Queue; + +/** + * An implementation of a task aggregator that executes tasks in the order that they are expected + * . All tasks that is given to {@link #execute(Object, Runnable)} must correspond to a key. This + * key must have been previously declared with {@link #expect(Object, Callback)}. + * The task will be scheduled to run when its corresponding key becomes the first expected key + * amongst the other keys in this aggregator. + * <p/> + * If on {@link #execute(Object, Runnable)} the key is not expected, the task will be executed + * immediately as an edge case. + * <p/> + * A characteristic scenario is as follows: + * <ol> + * <li>Expect keys <b>A</b>, <b>B</b>, <b>C</b>, and <b>Z</b>, in that order. Key <b>A</b> is now + * the first expected key.</li> + * <li>Execute task <b>2</b> for key <b>B</b>. The first expected key is <b>A</b>, + * which has no task associated with it, so we store task <b>2</b>. </li> + * <li>Execute task <b>4</b> for key <b>Z</b>. We store task <b>4</b>. </li> + * <li>Execute task <b>1</b> for key <b>A</b>. We run task <b>1</b> because its key <b>A</b> is + * the first expected, then we remove key <b>A</b> from the list of keys that we expect.</li> + * <li>This causes key <b>B</b> to be the first expected key, and we see that we have previously + * stored task <b>2</b> for that key. We run task <b>2</b> and remove key <b>B</b>. </li> + * <li>Key <b>C</b> is now the first expected key, but it has no task, + * so nothing happens. Task <b>4</b>, which was previously stored, + * cannot run until its corresponding key <b>Z</b> becomes the first expected key. This can + * happen if a task comes in for key <b>C</b> or if forget is called on key <b>C</b>.</li> + * </ol> + * <p/> + * ContiguousFIFOAggregator is not thread safe. + */ +public class ContiguousFIFOAggregator<T> { + private final Queue<T> mExpected; + private final SparseArray<Value> mTasks; + + private static final String TAG = ContiguousFIFOAggregator.class.getSimpleName(); + private static final boolean DEBUG = false; + + /** + * Create a new ContiguousFIFOAggregator. + * <p/> + * The nature of the prioritization means that the number of stored keys and tasks may grow + * unbounded. This may happen, for instance, if the top priority key is never given a task to + * {@link #execute(Object, Runnable)}. However, in practice, if you are generating tasks in + * response to UI elements appearing on the screen, you will only have a bounded set of keys. + * UI elements that scroll off the screen will call {@link #forget(Object)} while new elements + * will call {@link #expect(Object, Callback)}. This means that the expected + * number of keys and tasks is + * the maximum number of UI elements that you expect to show on the screen at any time. + */ + public ContiguousFIFOAggregator() { + mExpected = new ArrayDeque<T>(); + mTasks = new SparseArray<Value>(); + } + + /** + * Declare a key to be expected in the future. The order in which you expect keys is very + * important. Keys that are declared first are guaranteed to have their tasks run first. You + * must call either {@link #forget(Object)} or {@link #execute(Object, Runnable)} + * with this key in the future, or you will leak the key. + * + * If you call expect with a previously expected key, the key will be placed at the back of + * the expected queue and its callback will be replaced with this one. + * + * @param key the key to expect a task for. Use the same key when setting the task later + * with {@link #execute (Object, Runnable)}. + * @param callback the callback to notify when the key becomes the first expected key, or null. + */ + public void expect(final T key, final Callback<T> callback) { + if (key == null) { + throw new IllegalArgumentException("Do not use null keys."); + } + + Trace.beginSection("pool expect"); + final int hash = key.hashCode(); + if (contains(key)) { + mExpected.remove(key); + mTasks.remove(hash); + } + final boolean isFirst = mExpected.isEmpty(); + mExpected.offer(key); + mTasks.put(hash, new Value(callback, null)); + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator >> tasks: %s", prettyPrint())); + } + + if (isFirst) { + onFirstExpectedChanged(key); + } + Trace.endSection(); + } + + /** + * Remove a previously declared key that we no longer expect to execute a task for. This + * potentially allows another key to now become the first expected key, + * and so this may trigger one or more tasks to be executed. + * + * @param key the key previously declared in {@link #expect(Object, Callback)}. + * + */ + public void forget(final T key) { + if (key == null) { + throw new IllegalArgumentException("Do not use null keys."); + } + + if (!contains(key)) { + return; + } + + Trace.beginSection("pool forget"); + final boolean removedFirst = key.equals(mExpected.peek()); + mExpected.remove(key); + mTasks.delete(key.hashCode()); + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator < tasks: %s", prettyPrint())); + } + + final T second; + if (removedFirst && (second = mExpected.peek()) != null) { + // We removed the first key. The second key is now first. + onFirstExpectedChanged(second); + } + + maybeExecuteNow(); + Trace.endSection(); + } + + /** + * Attempt to execute a task corresponding to a previously declared key. If the key is the + * first expected key, the task will be executed immediately. Otherwise, the task is stored + * until its key becomes the first expected key. Execution of a task results in the removal + * of that key, which potentially allows another key to now become the first expected key, + * and may cause one or more other tasks to be executed. + * <p/> + * If the key is not expected, the task will be executed immediately as an edge case. + * + * @param key the key previously declared in {@link #expect(Object, Callback)}. + * @param task the task to execute or store, depending on its corresponding key. + */ + public void execute(final T key, final Runnable task) { + Trace.beginSection("pool execute"); + final int hash = key.hashCode(); + final Value value = mTasks.get(hash); + if (value == null || task == null) { + if (task != null) { + task.run(); + } + Trace.endSection(); + return; + } + value.task = task; + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator ++ tasks: %s", prettyPrint())); + } + maybeExecuteNow(); + Trace.endSection(); + } + + /** + * Triggered by {@link #execute(Object, Runnable)} and {@link #forget(Object)}. The keys or + * tasks have changed, which may cause one or more tasks to be executed. This method will + * continue to execute tasks associated with the first expected key to the last expected key, + * stopping when it finds that the first expected key has not yet been assigned a task. + */ + private void maybeExecuteNow() { + T first; + int count = 0; + while (!mExpected.isEmpty()) { + Trace.beginSection("pool maybeExecuteNow loop"); + first = mExpected.peek(); + if (count > 0) { + // When count == 0, the key is already first. + onFirstExpectedChanged(first); + } + + final int hash = first.hashCode(); + final Value value = mTasks.get(hash); + if (value.task == null) { + Trace.endSection(); + break; + } + + mExpected.poll(); + mTasks.delete(hash); + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator - tasks: %s", prettyPrint())); + } + value.task.run(); + count++; + Trace.endSection(); + } + } + + /** + * This method should only be called once for any key. + * @param key The key that has become the new first expected key. + */ + private void onFirstExpectedChanged(final T key) { + final int hash = key.hashCode(); + final Value value = mTasks.get(hash); + if (value == null) { + return; + } + final Callback<T> callback = value.callback; + if (callback == null) { + return; + } + if (DEBUG) { + Log.d(TAG, String.format("ContiguousFIFOAggregator first: %d", hash)); + } + callback.onBecomeFirstExpected(key); + } + + private boolean contains(final T key) { + return mTasks.get(key.hashCode()) != null; + } + + /** + * Get a pretty string representing the internal data. + * @return a String for the internal data. + */ + private String prettyPrint() { + if (mExpected.isEmpty()) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mExpected.size() * 28); + buffer.append('{'); + Iterator<T> it = mExpected.iterator(); + while (it.hasNext()) { + final T key = it.next(); + final int hash = key.hashCode(); + buffer.append(hash); + buffer.append('='); + final Value value = mTasks.get(hash); + buffer.append(value); + if (it.hasNext()) { + buffer.append(", "); + } + } + buffer.append('}'); + if (mExpected.size() != mTasks.size()) { + buffer.append(" error"); + } + return buffer.toString(); + } + + /** + * Implement this interface if you want to be notified when the key becomes the first + * expected key. + * @param <T> The type of the key used for the aggregator. + */ + public interface Callback<T> { + + /** + * The key you declared as expected has become the first expected key in this aggregator. + * + * We don't need a noLongerFirstExpected() method because this aggregator strictly adds + * additional to the end of the queue. For a first expected key to no longer be the first + * expected, it would either have to be forgotten with {@link #forget(Object)} or a task + * assigned and executed with {@link #execute(Object, Runnable)}. + * + * @param key The key that became first. We provide the key so the callback can either not + * keep state, or it can keep state which may have changed so the callback can do + * a comparison. + */ + void onBecomeFirstExpected(final T key); + } + + /** + * Holds the callback and task for when a key later becomes the first expected key. + */ + private class Value { + + final Callback<T> callback; + Runnable task; + + Value(final Callback<T> callback, final Runnable task) { + this.callback = callback; + this.task = task; + } + + @Override + public String toString() { + return String.valueOf(task); + } + } +} diff --git a/src/com/android/bitmap/DecodeAggregator.java b/src/com/android/bitmap/DecodeAggregator.java new file mode 100644 index 0000000..ec88efc --- /dev/null +++ b/src/com/android/bitmap/DecodeAggregator.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +import com.android.bitmap.DecodeTask.Request; + +public class DecodeAggregator extends ContiguousFIFOAggregator<Request> { + + public interface Callback extends ContiguousFIFOAggregator.Callback<Request> { + + } +} diff --git a/src/com/android/bitmap/DecodeTask.java b/src/com/android/bitmap/DecodeTask.java new file mode 100644 index 0000000..ab2a994 --- /dev/null +++ b/src/com/android/bitmap/DecodeTask.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.os.AsyncTask; +import android.util.Log; + +import com.android.bitmap.util.BitmapUtils; +import com.android.bitmap.util.Exif; +import com.android.bitmap.util.RectUtils; +import com.android.bitmap.util.Trace; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Decodes an image from either a file descriptor or input stream on a worker thread. After the + * decode is complete, even if the task is cancelled, the result is placed in the given cache. + * A {@link DecodeCallback} client may be notified on decode begin and completion. + * <p> + * This class uses {@link BitmapRegionDecoder} when possible to minimize unnecessary decoding + * and allow bitmap reuse on Jellybean 4.1 and later. + * <p> + * GIFs are supported, but their decode does not reuse bitmaps at all. The resulting + * {@link ReusableBitmap} will be marked as not reusable + * ({@link ReusableBitmap#isEligibleForPooling()} will return false). + */ +public class DecodeTask extends AsyncTask<Void, Void, ReusableBitmap> { + + private final Request mKey; + private final int mDestW; + private final int mDestH; + private final DecodeCallback mDecodeCallback; + private final BitmapCache mCache; + private final BitmapFactory.Options mOpts = new BitmapFactory.Options(); + + private ReusableBitmap mInBitmap = null; + + private static final boolean CROP_DURING_DECODE = true; + + private static final String TAG = DecodeTask.class.getSimpleName(); + private static final boolean DEBUG = false; + + /** + * The decode task uses this class to get input to decode. You must implement at least one of + * {@link #createFd()} or {@link #createInputStream()}. {@link DecodeTask} will prioritize + * {@link #createFd()} before falling back to {@link #createInputStream()}. + * <p> + * When {@link DecodeTask} is used in conjunction with a {@link BitmapCache}, objects of this + * type will also serve as cache keys to fetch cached data. + */ + public interface Request { + AssetFileDescriptor createFd() throws IOException; + InputStream createInputStream() throws IOException; + boolean hasOrientationExif() throws IOException; + } + + /** + * Callback interface for clients to be notified of decode state changes and completion. + */ + public interface DecodeCallback { + /** + * Notifies that the async task's work is about to begin. Up until this point, the task + * may have been preempted by the scheduler or queued up by a bottlenecked executor. + * <p> + * N.B. this method runs on the UI thread. + */ + void onDecodeBegin(Request key); + /** + * The task is now complete and the ReusableBitmap is available for use. Clients should + * double check that the request matches what the client is expecting. + */ + void onDecodeComplete(Request key, ReusableBitmap result); + /** + * The task has been canceled, and {@link #onDecodeComplete(Request, ReusableBitmap)} will + * not be called. + */ + void onDecodeCancel(Request key); + } + + public DecodeTask(Request key, int w, int h, DecodeCallback view, + BitmapCache cache) { + mKey = key; + mDestW = w; + mDestH = h; + mDecodeCallback = view; + mCache = cache; + } + + @Override + protected ReusableBitmap doInBackground(Void... params) { + // enqueue the 'onDecodeBegin' signal on the main thread + publishProgress(); + + return decode(); + } + + public ReusableBitmap decode() { + if (isCancelled()) { + return null; + } + + ReusableBitmap result = null; + AssetFileDescriptor fd = null; + InputStream in = null; + try { + final boolean isJellyBeanOrAbove = android.os.Build.VERSION.SDK_INT + >= android.os.Build.VERSION_CODES.JELLY_BEAN; + // This blocks during fling when the pool is empty. We block early to avoid jank. + if (isJellyBeanOrAbove) { + Trace.beginSection("poll for reusable bitmap"); + mInBitmap = mCache.poll(); + Trace.endSection(); + + if (isCancelled()) { + return null; + } + } + + Trace.beginSection("create fd and stream"); + fd = mKey.createFd(); + Trace.endSection(); + if (fd == null) { + in = reset(in); + if (in == null) { + return null; + } + } + + Trace.beginSection("get bytesize"); + final long byteSize; + if (fd != null) { + byteSize = fd.getLength(); + } else { + byteSize = -1; + } + Trace.endSection(); + + Trace.beginSection("get orientation"); + final int orientation; + if (mKey.hasOrientationExif()) { + if (fd != null) { + // Creating an input stream from the file descriptor makes it useless + // afterwards. + Trace.beginSection("create fd and stream"); + final AssetFileDescriptor orientationFd = mKey.createFd(); + in = orientationFd.createInputStream(); + Trace.endSection(); + } + orientation = Exif.getOrientation(in, byteSize); + if (fd != null) { + try { + // Close the temporary file descriptor. + in.close(); + } catch (IOException ignored) { + } + } + } else { + orientation = 0; + } + final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180; + Trace.endSection(); + + if (orientation != 0) { + // disable inBitmap-- bitmap reuse doesn't work with different decode regions due + // to orientation + if (mInBitmap != null) { + mCache.offer(mInBitmap); + mInBitmap = null; + mOpts.inBitmap = null; + } + } + + if (isCancelled()) { + return null; + } + + if (fd == null) { + in = reset(in); + if (in == null) { + return null; + } + } + + Trace.beginSection("decodeBounds"); + mOpts.inJustDecodeBounds = true; + if (fd != null) { + BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); + } else { + BitmapFactory.decodeStream(in, null, mOpts); + } + Trace.endSection(); + + if (isCancelled()) { + return null; + } + + // We want to calculate the sample size "as if" the orientation has been corrected. + final int srcW, srcH; // Orientation corrected. + if (isNotRotatedOr180) { + srcW = mOpts.outWidth; + srcH = mOpts.outHeight; + } else { + srcW = mOpts.outHeight; + srcH = mOpts.outWidth; + } + mOpts.inSampleSize = calculateSampleSize(srcW, srcH, mDestW, mDestH); + mOpts.inJustDecodeBounds = false; + mOpts.inMutable = true; + if (isJellyBeanOrAbove && orientation == 0) { + if (mInBitmap == null) { + if (DEBUG) { + Log.e(TAG, "decode thread wants a bitmap. cache dump:\n" + + mCache.toDebugString()); + } + Trace.beginSection("create reusable bitmap"); + mInBitmap = new ReusableBitmap(Bitmap.createBitmap(mDestW, mDestH, + Bitmap.Config.ARGB_8888)); + Trace.endSection(); + + if (isCancelled()) { + return null; + } + + if (DEBUG) { + Log.e(TAG, "*** allocated new bitmap in decode thread: " + + mInBitmap + " key=" + mKey); + } + } else { + if (DEBUG) { + Log.e(TAG, "*** reusing existing bitmap in decode thread: " + + mInBitmap + " key=" + mKey); + } + + } + mOpts.inBitmap = mInBitmap.bmp; + } + + if (isCancelled()) { + return null; + } + + if (fd == null) { + in = reset(in); + if (in == null) { + return null; + } + } + + Bitmap decodeResult = null; + final Rect srcRect = new Rect(); // Not orientation corrected. True coordinates. + if (CROP_DURING_DECODE) { + try { + Trace.beginSection("decodeCropped" + mOpts.inSampleSize); + decodeResult = decodeCropped(fd, in, orientation, srcRect); + } catch (IOException e) { + // fall through to below and try again with the non-cropping decoder + e.printStackTrace(); + } finally { + Trace.endSection(); + } + + if (isCancelled()) { + return null; + } + } + + //noinspection PointlessBooleanExpression + if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) { + try { + Trace.beginSection("decode" + mOpts.inSampleSize); + // disable inBitmap-- bitmap reuse doesn't work well below K + if (mInBitmap != null) { + mCache.offer(mInBitmap); + mInBitmap = null; + mOpts.inBitmap = null; + } + decodeResult = decode(fd, in); + } catch (IllegalArgumentException e) { + Log.e(TAG, "decode failed: reason='" + e.getMessage() + "' ss=" + + mOpts.inSampleSize); + + if (mOpts.inSampleSize > 1) { + // try again with ss=1 + mOpts.inSampleSize = 1; + decodeResult = decode(fd, in); + } + } finally { + Trace.endSection(); + } + + if (isCancelled()) { + return null; + } + } + + if (decodeResult == null) { + return null; + } + + if (mInBitmap != null) { + result = mInBitmap; + // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath + if (!srcRect.isEmpty()) { + result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize); + result.setLogicalHeight( + (srcRect.bottom - srcRect.top) / mOpts.inSampleSize); + } else { + result.setLogicalWidth(mOpts.outWidth); + result.setLogicalHeight(mOpts.outHeight); + } + } else { + // no mInBitmap means no pooling + result = new ReusableBitmap(decodeResult, false /* reusable */); + if (isNotRotatedOr180) { + result.setLogicalWidth(decodeResult.getWidth()); + result.setLogicalHeight(decodeResult.getHeight()); + } else { + result.setLogicalWidth(decodeResult.getHeight()); + result.setLogicalHeight(decodeResult.getWidth()); + } + } + result.setOrientation(orientation); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (fd != null) { + try { + fd.close(); + } catch (IOException ignored) { + } + } + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + if (result != null) { + result.acquireReference(); + mCache.put(mKey, result); + if (DEBUG) { + Log.d(TAG, "placed result in cache: key=" + mKey + " bmp=" + + result + " cancelled=" + isCancelled()); + } + } else if (mInBitmap != null) { + if (DEBUG) { + Log.d(TAG, "placing failed/cancelled bitmap in pool: key=" + + mKey + " bmp=" + mInBitmap); + } + mCache.offer(mInBitmap); + } + } + return result; + } + + private Bitmap decodeCropped(final AssetFileDescriptor fd, final InputStream in, + final int orientation, final Rect outSrcRect) throws IOException { + final BitmapRegionDecoder brd; + if (fd != null) { + brd = BitmapRegionDecoder.newInstance(fd.getFileDescriptor(), true /* shareable */); + } else { + brd = BitmapRegionDecoder.newInstance(in, true /* shareable */); + } + if (isCancelled()) { + brd.recycle(); + return null; + } + + // We want to call calculateCroppedSrcRect() on the source rectangle "as if" the + // orientation has been corrected. + final int srcW, srcH; //Orientation corrected. + final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180; + if (isNotRotatedOr180) { + srcW = mOpts.outWidth; + srcH = mOpts.outHeight; + } else { + srcW = mOpts.outHeight; + srcH = mOpts.outWidth; + } + + // Coordinates are orientation corrected. + // Center the decode on the top 1/3. + BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDestW, mDestH, mDestH, mOpts.inSampleSize, + 1f / 3, true /* absoluteFraction */, 1f, outSrcRect); + if (DEBUG) System.out.println("rect for this decode is: " + outSrcRect + + " srcW/H=" + srcW + "/" + srcH + + " dstW/H=" + mDestW + "/" + mDestH); + + // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has + // been corrected. We need to decode the uncorrected source rectangle. Calculate true + // coordinates. + RectUtils.rotateRectForOrientation(orientation, new Rect(0, 0, srcW, srcH), outSrcRect); + + final Bitmap result = brd.decodeRegion(outSrcRect, mOpts); + brd.recycle(); + return result; + } + + /** + * Return an input stream that can be read from the beginning using the most efficient way, + * given an input stream that may or may not support reset(), or given null. + * + * The returned input stream may or may not be the same stream. + */ + private InputStream reset(InputStream in) throws IOException { + Trace.beginSection("create stream"); + if (in == null) { + in = mKey.createInputStream(); + } else if (in.markSupported()) { + in.reset(); + } else { + try { + in.close(); + } catch (IOException ignored) { + } + in = mKey.createInputStream(); + } + Trace.endSection(); + return in; + } + + private Bitmap decode(AssetFileDescriptor fd, InputStream in) { + final Bitmap result; + if (fd != null) { + result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts); + } else { + result = BitmapFactory.decodeStream(in, null, mOpts); + } + return result; + } + + private static int calculateSampleSize(int srcW, int srcH, int destW, int destH) { + int result; + + final float sz = Math.min((float) srcW / destW, (float) srcH / destH); + + // round to the nearest power of two, or just truncate + final boolean stricter = true; + + //noinspection ConstantConditions + if (stricter) { + result = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2)))); + } else { + result = (int) sz; + } + return Math.max(1, result); + } + + public void cancel() { + cancel(true); + mOpts.requestCancelDecode(); + } + + @Override + protected void onProgressUpdate(Void... values) { + mDecodeCallback.onDecodeBegin(mKey); + } + + @Override + public void onPostExecute(ReusableBitmap result) { + mDecodeCallback.onDecodeComplete(mKey, result); + } + + @Override + protected void onCancelled(ReusableBitmap result) { + mDecodeCallback.onDecodeCancel(mKey); + if (result == null) { + return; + } + + result.releaseReference(); + if (mInBitmap == null) { + // not reusing bitmaps: can recycle immediately + result.bmp.recycle(); + } + } + +} diff --git a/src/com/android/bitmap/Poolable.java b/src/com/android/bitmap/Poolable.java new file mode 100644 index 0000000..c68a25c --- /dev/null +++ b/src/com/android/bitmap/Poolable.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +public interface Poolable { + void acquireReference(); + void releaseReference(); + int getRefCount(); + boolean isEligibleForPooling(); +} diff --git a/src/com/android/bitmap/PooledCache.java b/src/com/android/bitmap/PooledCache.java new file mode 100644 index 0000000..6d6684f --- /dev/null +++ b/src/com/android/bitmap/PooledCache.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +public interface PooledCache<K, V> { + + V get(K key, boolean incrementRefCount); + V put(K key, V value); + void offer(V scrapValue); + V poll(); + String toDebugString(); + + /** + * Purge existing Poolables from the pool+cache. Usually, this is done when situations + * change and the items in the pool+cache are no longer appropriate. For example, + * if the layout changes, the pool+cache may need to hold larger bitmaps. + * + * <p/> + * The existing Poolables will be garbage collected when they are no longer being referenced + * by other objects. + */ + void clear(); +} diff --git a/src/com/android/bitmap/ReusableBitmap.java b/src/com/android/bitmap/ReusableBitmap.java new file mode 100644 index 0000000..dde9bd1 --- /dev/null +++ b/src/com/android/bitmap/ReusableBitmap.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +import android.graphics.Bitmap; + +/** + * A simple bitmap wrapper. Currently supports reference counting and logical width/height + * (which may differ from a bitmap's reported width/height due to bitmap reuse). + */ +public class ReusableBitmap implements Poolable { + + public final Bitmap bmp; + private int mWidth; + private int mHeight; + private int mOrientation; + + private int mRefCount = 0; + private final boolean mReusable; + + public ReusableBitmap(final Bitmap bitmap) { + this(bitmap, true /* reusable */); + } + + public ReusableBitmap(final Bitmap bitmap, final boolean reusable) { + bmp = bitmap; + mReusable = reusable; + } + + @Override + public boolean isEligibleForPooling() { + return mReusable; + } + + public void setLogicalWidth(int w) { + mWidth = w; + } + + public void setLogicalHeight(int h) { + mHeight = h; + } + + public int getLogicalWidth() { + return mWidth; + } + + public int getLogicalHeight() { + return mHeight; + } + + public int getOrientation() { + return mOrientation; + } + + public void setOrientation(final int orientation) { + mOrientation = orientation; + } + + public int getByteCount() { + return bmp.getByteCount(); + } + + @Override + public void acquireReference() { + mRefCount++; + } + + @Override + public void releaseReference() { + if (mRefCount == 0) { + throw new IllegalStateException(); + } + mRefCount--; + } + + @Override + public int getRefCount() { + return mRefCount; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("["); + sb.append(super.toString()); + sb.append(" refCount="); + sb.append(mRefCount); + sb.append(" mReusable="); + sb.append(mReusable); + sb.append(" bmp="); + sb.append(bmp); + sb.append(" logicalW/H="); + sb.append(mWidth); + sb.append("/"); + sb.append(mHeight); + if (bmp != null) { + sb.append(" sz="); + sb.append(bmp.getByteCount() >> 10); + sb.append("KB"); + } + sb.append("]"); + return sb.toString(); + } + + /** + * Singleton class to represent a null Bitmap. We don't want to just use a regular + * ReusableBitmap with a null bmp field because that will render that ReusableBitmap useless + * and unable to be used by another decode process. + */ + public final static class NullReusableBitmap extends ReusableBitmap { + private static NullReusableBitmap sInstance; + + /** + * Get a singleton. + */ + public static NullReusableBitmap getInstance() { + if (sInstance == null) { + sInstance = new NullReusableBitmap(); + } + return sInstance; + } + + private NullReusableBitmap() { + super(null /* bmp */, false /* reusable */); + } + + @Override + public int getByteCount() { + return 0; + } + + @Override + public void releaseReference() { } + + @Override + public void acquireReference() { } + } +} diff --git a/src/com/android/bitmap/UnrefedBitmapCache.java b/src/com/android/bitmap/UnrefedBitmapCache.java new file mode 100644 index 0000000..6c9db73 --- /dev/null +++ b/src/com/android/bitmap/UnrefedBitmapCache.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +import android.util.Log; +import android.util.LruCache; + +import com.android.bitmap.DecodeTask.Request; +import com.android.bitmap.ReusableBitmap.NullReusableBitmap; +import com.android.bitmap.util.Trace; + +/** + * This subclass provides custom pool behavior. The pool can be set to block on {@link #poll()} if + * nothing can be returned. This is useful if you know you will incur high costs upon receiving + * nothing from the pool, and you do not want to incur those costs at the critical moment when the + * UI is animating. + * + * This subclass provides custom cache behavior. Null values can be cached. Later, + * when the same key is used to retrieve the value, a {@link NullReusableBitmap} singleton will + * be returned. + */ +public class UnrefedBitmapCache extends UnrefedPooledCache<Request, ReusableBitmap> + implements BitmapCache { + private boolean mBlocking = false; + private final Object mLock = new Object(); + + private LruCache<Request, NullReusableBitmap> mNullRequests; + + private final static boolean DEBUG = false; + private final static String TAG = UnrefedBitmapCache.class.getSimpleName(); + + public UnrefedBitmapCache(final int targetSizeBytes, final float nonPooledFraction, + final int nullCapacity) { + super(targetSizeBytes, nonPooledFraction); + + if (nullCapacity > 0) { + mNullRequests = new LruCache<Request, NullReusableBitmap>(nullCapacity); + } + } + + /** + * Declare that {@link #poll()} should now block until it can return something. + */ + @Override + public void setBlocking(final boolean blocking) { + synchronized (mLock) { + if (DEBUG) { + Log.d(TAG, String.format("AltBitmapCache: block %b", blocking)); + } + mBlocking = blocking; + if (!mBlocking) { + // no longer blocking. Notify every thread. + mLock.notifyAll(); + } + } + } + + @Override + protected int sizeOf(final ReusableBitmap value) { + return value.getByteCount(); + } + + /** + * If {@link #setBlocking(boolean)} has been called with true, this method will block until a + * resource is available. + * @return an available resource, or null if none are available. Null will never be returned + * until blocking is set to false. + */ + @Override + public ReusableBitmap poll() { + ReusableBitmap bitmap; + synchronized (mLock) { + while ((bitmap = super.poll()) == null && mBlocking) { + if (DEBUG) { + Log.d(TAG, String.format( + "AltBitmapCache: %s waiting", Thread.currentThread().getName())); + } + Trace.beginSection("sleep"); + try { + // block + mLock.wait(); + if (DEBUG) { + Log.d(TAG, String.format("AltBitmapCache: %s notified", + Thread.currentThread().getName())); + } + } catch (InterruptedException ignored) { + } + Trace.endSection(); + } + } + return bitmap; + } + + @Override + public void offer(final ReusableBitmap value) { + synchronized (mLock) { + super.offer(value); + if (DEBUG) { + Log.d(TAG, "AltBitmapCache: offer +1"); + } + // new resource gained. Notify one thread. + mLock.notify(); + } + } + + @Override + public ReusableBitmap get(final Request key, final boolean incrementRefCount) { + if (mNullRequests != null && mNullRequests.get(key) != null) { + return NullReusableBitmap.getInstance(); + } + return super.get(key, incrementRefCount); + } + + /** + * Note: The cache only supports same-sized bitmaps. + */ + @Override + public ReusableBitmap put(final Request key, final ReusableBitmap value) { + if (mNullRequests != null && (value == null || value == NullReusableBitmap.getInstance())) { + mNullRequests.put(key, NullReusableBitmap.getInstance()); + return null; + } + + return super.put(key, value); + } +} diff --git a/src/com/android/bitmap/UnrefedPooledCache.java b/src/com/android/bitmap/UnrefedPooledCache.java new file mode 100644 index 0000000..ff0ab7b --- /dev/null +++ b/src/com/android/bitmap/UnrefedPooledCache.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2013 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.android.bitmap; + +import com.android.bitmap.util.Trace; + +import android.util.Log; +import android.util.LruCache; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * An alternative implementation of a pool+cache. This implementation only counts + * unreferenced objects in its size calculation. Internally, it never evicts from + * its cache, and instead {@link #poll()} is allowed to return unreferenced cache + * entries. + * <p> + * You would only use this kind of cache if your objects are interchangeable and + * have significant allocation cost, and if your memory footprint is somewhat + * flexible. + * <p> + * Because this class only counts unreferenced objects toward targetSize, + * it will have a total memory footprint of: + * <code>(targetSize) + (# of threads concurrently writing to cache) + + * (total size of still-referenced entries)</code> + * + */ +public class UnrefedPooledCache<K, V extends Poolable> implements PooledCache<K, V> { + + private final LinkedHashMap<K, V> mCache; + private final LinkedBlockingQueue<V> mPool; + private final int mTargetSize; + private final LruCache<K, V> mNonPooledCache; + + private static final boolean DEBUG = false; + private static final String TAG = UnrefedPooledCache.class.getSimpleName(); + + /** + * @param targetSize not exactly a max size in practice + * @param nonPooledFraction the fractional portion in the range [0.0,1.0] of targetSize to + * dedicate to non-poolable entries + */ + public UnrefedPooledCache(int targetSize, float nonPooledFraction) { + mCache = new LinkedHashMap<K, V>(0, 0.75f, true); + mPool = new LinkedBlockingQueue<V>(); + final int nonPooledSize = Math.round(targetSize * nonPooledFraction); + if (nonPooledSize > 0) { + mNonPooledCache = new NonPooledCache(nonPooledSize); + } else { + mNonPooledCache = null; + } + mTargetSize = targetSize - nonPooledSize; + } + + @Override + public V get(K key, boolean incrementRefCount) { + Trace.beginSection("cache get"); + synchronized (mCache) { + V result = mCache.get(key); + if (result == null && mNonPooledCache != null) { + result = mNonPooledCache.get(key); + } + if (incrementRefCount && result != null) { + result.acquireReference(); + } + Trace.endSection(); + return result; + } + } + + @Override + public V put(K key, V value) { + Trace.beginSection("cache put"); + synchronized (mCache) { + final V prev; + if (value.isEligibleForPooling()) { + prev = mCache.put(key, value); + } else if (mNonPooledCache != null) { + prev = mNonPooledCache.put(key, value); + } else { + prev = null; + } + Trace.endSection(); + return prev; + } + } + + @Override + public void offer(V value) { + Trace.beginSection("pool offer"); + if (value.getRefCount() != 0 || !value.isEligibleForPooling()) { + throw new IllegalArgumentException("unexpected offer of an invalid object: " + value); + } + mPool.offer(value); + Trace.endSection(); + } + + @Override + public V poll() { + Trace.beginSection("pool poll"); + final V pooled = mPool.poll(); + if (pooled != null) { + Trace.endSection(); + return pooled; + } + + synchronized (mCache) { + int unrefSize = 0; + Map.Entry<K, V> eldestUnref = null; + for (Map.Entry<K, V> entry : mCache.entrySet()) { + final V value = entry.getValue(); + if (value.getRefCount() > 0 || !value.isEligibleForPooling()) { + continue; + } + if (eldestUnref == null) { + eldestUnref = entry; + } + unrefSize += sizeOf(value); + if (unrefSize > mTargetSize) { + break; + } + } + // only return a scavenged cache entry if the cache has enough + // eligible (unreferenced) items + if (unrefSize <= mTargetSize) { + if (DEBUG) { + Log.e(TAG, "POOL SCAVENGE FAILED, cache not fully warm yet. szDelta=" + + (mTargetSize-unrefSize)); + } + Trace.endSection(); + return null; + } else { + mCache.remove(eldestUnref.getKey()); + if (DEBUG) { + Log.e(TAG, "POOL SCAVENGE SUCCESS, oldKey=" + eldestUnref.getKey()); + } + Trace.endSection(); + return eldestUnref.getValue(); + } + } + } + + protected int sizeOf(V value) { + return 1; + } + + @Override + public String toDebugString() { + if (DEBUG) { + final StringBuilder sb = new StringBuilder("["); + sb.append(super.toString()); + int size = 0; + synchronized (mCache) { + sb.append(" poolCount="); + sb.append(mPool.size()); + sb.append(" cacheSize="); + sb.append(mCache.size()); + if (mNonPooledCache != null) { + sb.append(" nonPooledCacheSize="); + sb.append(mNonPooledCache.size()); + } + sb.append("\n---------------------"); + for (V val : mPool) { + size += sizeOf(val); + sb.append("\n\tpool item: "); + sb.append(val); + } + sb.append("\n---------------------"); + for (Map.Entry<K, V> item : mCache.entrySet()) { + final V val = item.getValue(); + sb.append("\n\tcache key="); + sb.append(item.getKey()); + sb.append(" val="); + sb.append(val); + size += sizeOf(val); + } + sb.append("\n---------------------"); + if (mNonPooledCache != null) { + for (Map.Entry<K, V> item : mNonPooledCache.snapshot().entrySet()) { + final V val = item.getValue(); + sb.append("\n\tnon-pooled cache key="); + sb.append(item.getKey()); + sb.append(" val="); + sb.append(val); + size += sizeOf(val); + } + sb.append("\n---------------------"); + } + sb.append("\nTOTAL SIZE=" + size); + } + sb.append("]"); + return sb.toString(); + } else { + return null; + } + } + + private class NonPooledCache extends LruCache<K, V> { + + public NonPooledCache(int maxSize) { + super(maxSize); + } + + @Override + protected int sizeOf(K key, V value) { + return UnrefedPooledCache.this.sizeOf(value); + } + + } + + @Override + public void clear() { + mCache.clear(); + mPool.clear(); + } +} diff --git a/src/com/android/bitmap/drawable/BasicBitmapDrawable.java b/src/com/android/bitmap/drawable/BasicBitmapDrawable.java new file mode 100644 index 0000000..b16618b --- /dev/null +++ b/src/com/android/bitmap/drawable/BasicBitmapDrawable.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2013 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.android.bitmap.drawable; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.Log; + +import com.android.bitmap.BitmapCache; +import com.android.bitmap.DecodeTask; +import com.android.bitmap.DecodeTask.Request; +import com.android.bitmap.ReusableBitmap; +import com.android.bitmap.util.BitmapUtils; +import com.android.bitmap.util.RectUtils; +import com.android.bitmap.util.Trace; + +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * This class encapsulates the basic functionality needed to display a single image bitmap, + * including request creation/cancelling, and data unbinding and re-binding. + * <p> + * The actual bitmap decode work is handled by {@link DecodeTask}. + */ +public class BasicBitmapDrawable extends Drawable implements DecodeTask.DecodeCallback, + Drawable.Callback { + + private BitmapRequestKey mCurrKey; + private ReusableBitmap mBitmap; + private final BitmapCache mCache; + private DecodeTask mTask; + private int mDecodeWidth; + private int mDecodeHeight; + + // based on framework CL:I015d77 + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = CPU_COUNT + 1; + private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor( + CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 1, TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>(128)); + + private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR; + + private static final boolean LIMIT_BITMAP_DENSITY = true; + + private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH; + + private final float mDensity; + private final Paint mPaint = new Paint(); + private final Rect mSrcRect = new Rect(); + + private static final String TAG = BasicBitmapDrawable.class.getSimpleName(); + private static final boolean DEBUG = false; + + public BasicBitmapDrawable(final Resources res, final BitmapCache cache) { + mDensity = res.getDisplayMetrics().density; + mCache = cache; + mPaint.setFilterBitmap(true); + } + + public DecodeTask.Request getKey() { + return mCurrKey; + } + + /** + * Set the dimensions to decode into. + */ + public void setDecodeDimensions(int w, int h) { + mDecodeWidth = w; + mDecodeHeight = h; + decode(); + } + + public void unbind() { + setImage(null); + } + + public void bind(BitmapRequestKey key) { + setImage(key); + } + + private void setImage(final BitmapRequestKey key) { + if (mCurrKey != null && mCurrKey.equals(key)) { + return; + } + + Trace.beginSection("set image"); + Trace.beginSection("release reference"); + if (mBitmap != null) { + mBitmap.releaseReference(); + mBitmap = null; + } + Trace.endSection(); + mCurrKey = key; + + if (mTask != null) { + mTask.cancel(); + mTask = null; + } + + if (key == null) { + invalidateSelf(); + Trace.endSection(); + return; + } + + // find cached entry here and skip decode if found. + final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */); + if (cached != null) { + setBitmap(cached); + if (DEBUG) { + Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey)); + } + } else { + decode(); + if (DEBUG) { + Log.d(TAG, String.format( + "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString())); + } + } + Trace.endSection(); + } + + @Override + public void draw(final Canvas canvas) { + final Rect bounds = getBounds(); + if (bounds.isEmpty()) { + return; + } + + if (mBitmap != null && mBitmap.bmp != null) { + BitmapUtils.calculateCroppedSrcRect( + mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), + bounds.width(), bounds.height(), + bounds.height(), Integer.MAX_VALUE, + 0.5f, false /* absoluteFraction */, + 1, mSrcRect); + + final int orientation = mBitmap.getOrientation(); + // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has + // been corrected. We need to decode the uncorrected source rectangle. Calculate true + // coordinates. + RectUtils.rotateRectForOrientation(orientation, + new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()), + mSrcRect); + + // We may need to rotate the canvas, so we also have to rotate the bounds. + final Rect rotatedBounds = new Rect(bounds); + RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds); + + // Rotate the canvas. + canvas.save(); + canvas.rotate(orientation, bounds.centerX(), bounds.centerY()); + canvas.drawBitmap(mBitmap.bmp, mSrcRect, rotatedBounds, mPaint); + canvas.restore(); + } + } + + @Override + public void setAlpha(int alpha) { + final int old = mPaint.getAlpha(); + mPaint.setAlpha(alpha); + if (alpha != old) { + invalidateSelf(); + } + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + invalidateSelf(); + } + + @Override + public int getOpacity() { + return (mBitmap != null && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ? + PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; + } + + @Override + public void onDecodeBegin(final Request key) { } + + @Override + public void onDecodeComplete(final Request key, final ReusableBitmap result) { + if (key.equals(mCurrKey)) { + setBitmap(result); + } else { + // if the requests don't match (i.e. this request is stale), decrement the + // ref count to allow the bitmap to be pooled + if (result != null) { + result.releaseReference(); + } + } + } + + @Override + public void onDecodeCancel(final Request key) { } + + private void setBitmap(ReusableBitmap bmp) { + if (mBitmap != null && mBitmap != bmp) { + mBitmap.releaseReference(); + } + mBitmap = bmp; + invalidateSelf(); + } + + private void decode() { + final int bufferW; + final int bufferH; + + if (mCurrKey == null) { + return; + } + + Trace.beginSection("decode"); + if (LIMIT_BITMAP_DENSITY) { + final float scale = + Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT + / mDensity); + bufferW = (int) (mDecodeWidth * scale); + bufferH = (int) (mDecodeHeight * scale); + } else { + bufferW = mDecodeWidth; + bufferH = mDecodeHeight; + } + + if (bufferW == 0 || bufferH == 0) { + Trace.endSection(); + return; + } + if (mTask != null) { + mTask.cancel(); + } + mTask = new DecodeTask(mCurrKey, bufferW, bufferH, this, mCache); + mTask.executeOnExecutor(EXECUTOR); + Trace.endSection(); + } + + @Override + public void invalidateDrawable(Drawable who) { + invalidateSelf(); + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } +} diff --git a/src/com/android/bitmap/drawable/BitmapRequestKey.java b/src/com/android/bitmap/drawable/BitmapRequestKey.java new file mode 100644 index 0000000..be3a7a1 --- /dev/null +++ b/src/com/android/bitmap/drawable/BitmapRequestKey.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 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.android.bitmap.drawable; + +import android.content.res.AssetFileDescriptor; +import android.text.TextUtils; + +import com.android.bitmap.DecodeTask; + +import java.io.IOException; +import java.io.InputStream; + +/* + * Extend this base class to return either createFd() or createInputStream(). + */ +public abstract class BitmapRequestKey implements DecodeTask.Request { + public final String mUriString; + + public BitmapRequestKey(String uriString) { + this.mUriString = uriString; + } + + @Override + public abstract boolean equals(Object o); + + @Override + public abstract int hashCode(); + + @Override + public abstract String toString(); + + @Override + public AssetFileDescriptor createFd() throws IOException { + return null; + } + + @Override + public InputStream createInputStream() throws IOException { + return null; + } + + @Override + public boolean hasOrientationExif() throws IOException { + return false; + } +}
\ No newline at end of file diff --git a/src/com/android/bitmap/drawable/ExtendedBitmapDrawable.java b/src/com/android/bitmap/drawable/ExtendedBitmapDrawable.java new file mode 100644 index 0000000..f3a3301 --- /dev/null +++ b/src/com/android/bitmap/drawable/ExtendedBitmapDrawable.java @@ -0,0 +1,579 @@ +/* + * Copyright (C) 2013 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.android.bitmap.drawable; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.animation.LinearInterpolator; + +import com.android.bitmap.R; +import com.android.bitmap.BitmapCache; +import com.android.bitmap.DecodeAggregator; +import com.android.bitmap.DecodeTask; +import com.android.bitmap.DecodeTask.Request; +import com.android.bitmap.ReusableBitmap; +import com.android.bitmap.util.BitmapUtils; +import com.android.bitmap.util.RectUtils; +import com.android.bitmap.util.Trace; + +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * This class encapsulates all functionality needed to display a single image bitmap, + * including request creation/cancelling, data unbinding and re-binding, and fancy animations + * to draw upon state changes. + * <p> + * The actual bitmap decode work is handled by {@link DecodeTask}. + * TODO: have this class extend from BasicBitmapDrawable + */ +public class ExtendedBitmapDrawable extends Drawable implements DecodeTask.DecodeCallback, + Drawable.Callback, Runnable, Parallaxable, DecodeAggregator.Callback { + + private BitmapRequestKey mCurrKey; + private ReusableBitmap mBitmap; + private final BitmapCache mCache; + private DecodeAggregator mDecodeAggregator; + private DecodeTask mTask; + private int mDecodeWidth; + private int mDecodeHeight; + private int mLoadState = LOAD_STATE_UNINITIALIZED; + private float mParallaxFraction = 0.5f; + private float mParallaxSpeedMultiplier; + + // each attachment gets its own placeholder and progress indicator, to be shown, hidden, + // and animated based on Drawable#setVisible() changes, which are in turn driven by + // #setLoadState(). + private Placeholder mPlaceholder; + private Progress mProgress; + + private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(4, 4, + 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); + + private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR; + + private static final boolean LIMIT_BITMAP_DENSITY = true; + + private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH; + + private static final int LOAD_STATE_UNINITIALIZED = 0; + private static final int LOAD_STATE_NOT_YET_LOADED = 1; + private static final int LOAD_STATE_LOADING = 2; + private static final int LOAD_STATE_LOADED = 3; + private static final int LOAD_STATE_FAILED = 4; + + private final float mDensity; + private int mProgressDelayMs; + private final Paint mPaint = new Paint(); + private final Rect mSrcRect = new Rect(); + private final Handler mHandler = new Handler(); + + public static final boolean DEBUG = false; + public static final String TAG = ExtendedBitmapDrawable.class.getSimpleName(); + + public ExtendedBitmapDrawable(final Resources res, final BitmapCache cache, + final DecodeAggregator decodeAggregator, final Drawable placeholder, + final Drawable progress) { + mDensity = res.getDisplayMetrics().density; + mCache = cache; + this.mDecodeAggregator = decodeAggregator; + mPaint.setFilterBitmap(true); + + final int fadeOutDurationMs = res.getInteger(R.integer.bitmap_fade_animation_duration); + final int tileColor = res.getColor(R.color.bitmap_placeholder_background_color); + mProgressDelayMs = res.getInteger(R.integer.bitmap_progress_animation_delay); + + int placeholderSize = res.getDimensionPixelSize(R.dimen.placeholder_size); + mPlaceholder = new Placeholder(placeholder.getConstantState().newDrawable(res), res, + placeholderSize, placeholderSize, fadeOutDurationMs, tileColor); + mPlaceholder.setCallback(this); + + int progressBarSize = res.getDimensionPixelSize(R.dimen.progress_bar_size); + mProgress = new Progress(progress.getConstantState().newDrawable(res), res, + progressBarSize, progressBarSize, fadeOutDurationMs, tileColor); + mProgress.setCallback(this); + } + + public DecodeTask.Request getKey() { + return mCurrKey; + } + + /** + * Set the dimensions to which to decode into. For a parallax effect, ensure the height is + * larger than the destination of the bitmap. + * TODO: test parallax + */ + public void setDecodeDimensions(int w, int h) { + mDecodeWidth = w; + mDecodeHeight = h; + decode(); + } + + public void setParallaxSpeedMultiplier(final float parallaxSpeedMultiplier) { + mParallaxSpeedMultiplier = parallaxSpeedMultiplier; + } + + public void showStaticPlaceholder() { + setLoadState(LOAD_STATE_FAILED); + } + + public void unbind() { + setImage(null); + } + + public void bind(BitmapRequestKey key) { + setImage(key); + } + + private void setImage(final BitmapRequestKey key) { + if (mCurrKey != null && mCurrKey.equals(key)) { + return; + } + + Trace.beginSection("set image"); + Trace.beginSection("release reference"); + if (mBitmap != null) { + mBitmap.releaseReference(); + mBitmap = null; + } + Trace.endSection(); + if (mCurrKey != null && mDecodeAggregator != null) { + mDecodeAggregator.forget(mCurrKey); + } + mCurrKey = key; + + if (mTask != null) { + mTask.cancel(); + mTask = null; + } + + mHandler.removeCallbacks(this); + // start from a clean slate on every bind + // this allows the initial transition to be specially instantaneous, so e.g. a cache hit + // doesn't unnecessarily trigger a fade-in + setLoadState(LOAD_STATE_UNINITIALIZED); + + if (key == null) { + invalidateSelf(); + Trace.endSection(); + return; + } + + // find cached entry here and skip decode if found. + final ReusableBitmap cached = mCache.get(key, true /* incrementRefCount */); + if (cached != null) { + setBitmap(cached); + if (DEBUG) { + Log.d(TAG, String.format("CACHE HIT key=%s", mCurrKey)); + } + } else { + decode(); + if (DEBUG) { + Log.d(TAG, String.format( + "CACHE MISS key=%s\ncache=%s", mCurrKey, mCache.toDebugString())); + } + } + Trace.endSection(); + } + + @Override + public void setParallaxFraction(float fraction) { + mParallaxFraction = fraction; + } + + @Override + public void draw(final Canvas canvas) { + final Rect bounds = getBounds(); + if (bounds.isEmpty()) { + return; + } + + if (mBitmap != null && mBitmap.bmp != null) { + BitmapUtils.calculateCroppedSrcRect( + mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), + bounds.width(), bounds.height(), + bounds.height(), Integer.MAX_VALUE, + mParallaxFraction, false /* absoluteFraction */, + mParallaxSpeedMultiplier, mSrcRect); + + final int orientation = mBitmap.getOrientation(); + // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has + // been corrected. We need to decode the uncorrected source rectangle. Calculate true + // coordinates. + RectUtils.rotateRectForOrientation(orientation, + new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()), + mSrcRect); + + // We may need to rotate the canvas, so we also have to rotate the bounds. + final Rect rotatedBounds = new Rect(bounds); + RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds); + + // Rotate the canvas. + canvas.save(); + canvas.rotate(orientation, bounds.centerX(), bounds.centerY()); + canvas.drawBitmap(mBitmap.bmp, mSrcRect, rotatedBounds, mPaint); + canvas.restore(); + } + + // Draw the two possible overlay layers in reverse-priority order. + // (each layer will no-op the draw when appropriate) + // This ordering means cross-fade transitions are just fade-outs of each layer. + mProgress.draw(canvas); + mPlaceholder.draw(canvas); + } + + @Override + public void setAlpha(int alpha) { + final int old = mPaint.getAlpha(); + mPaint.setAlpha(alpha); + mPlaceholder.setAlpha(alpha); + mProgress.setAlpha(alpha); + if (alpha != old) { + invalidateSelf(); + } + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + mPlaceholder.setColorFilter(cf); + mProgress.setColorFilter(cf); + invalidateSelf(); + } + + @Override + public int getOpacity() { + return (mBitmap != null && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ? + PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + mPlaceholder.setBounds(bounds); + mProgress.setBounds(bounds); + } + + @Override + public void onDecodeBegin(final Request key) { + if (mDecodeAggregator != null) { + mDecodeAggregator.expect(key, this); + } else { + onBecomeFirstExpected(key); + } + } + + @Override + public void onBecomeFirstExpected(final Request key) { + if (!key.equals(mCurrKey)) { + return; + } + // normally, we'd transition to the LOADING state now, but we want to delay that a bit + // to minimize excess occurrences of the rotating spinner + mHandler.postDelayed(this, mProgressDelayMs); + } + + @Override + public void run() { + if (mLoadState == LOAD_STATE_NOT_YET_LOADED) { + setLoadState(LOAD_STATE_LOADING); + } + } + + @Override + public void onDecodeComplete(final Request key, final ReusableBitmap result) { + if (mDecodeAggregator != null) { + mDecodeAggregator.execute(key, new Runnable() { + @Override + public void run() { + onDecodeCompleteImpl(key, result); + } + + @Override + public String toString() { + return "DONE"; + } + }); + } else { + onDecodeCompleteImpl(key, result); + } + } + + private void onDecodeCompleteImpl(final Request key, final ReusableBitmap result) { + if (key.equals(mCurrKey)) { + setBitmap(result); + } else { + // if the requests don't match (i.e. this request is stale), decrement the + // ref count to allow the bitmap to be pooled + if (result != null) { + result.releaseReference(); + } + } + } + + @Override + public void onDecodeCancel(final Request key) { + if (mDecodeAggregator != null) { + mDecodeAggregator.forget(key); + } + } + + private void setBitmap(ReusableBitmap bmp) { + if (mBitmap != null && mBitmap != bmp) { + mBitmap.releaseReference(); + } + mBitmap = bmp; + setLoadState((bmp != null) ? LOAD_STATE_LOADED : LOAD_STATE_FAILED); + invalidateSelf(); + } + + private void decode() { + final int bufferW; + final int bufferH; + + if (mCurrKey == null) { + return; + } + + Trace.beginSection("decode"); + if (LIMIT_BITMAP_DENSITY) { + final float scale = + Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT + / mDensity); + bufferW = (int) (mDecodeWidth * scale); + bufferH = (int) (mDecodeHeight * scale); + } else { + bufferW = mDecodeWidth; + bufferH = mDecodeHeight; + } + + if (bufferW == 0 || bufferH == 0) { + Trace.endSection(); + return; + } + if (mTask != null) { + mTask.cancel(); + } + setLoadState(LOAD_STATE_NOT_YET_LOADED); + mTask = new DecodeTask(mCurrKey, bufferW, bufferH, this, mCache); + mTask.executeOnExecutor(EXECUTOR); + Trace.endSection(); + } + + private void setLoadState(int loadState) { + if (DEBUG) { + Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s", + mLoadState, loadState, mCurrKey, this)); + } + if (mLoadState == loadState) { + if (DEBUG) { + Log.v(TAG, "OUT no-op setLoadState"); + } + return; + } + + Trace.beginSection("set load state"); + switch (loadState) { + // This state differs from LOADED in that the subsequent state transition away from + // UNINITIALIZED will not have a fancy transition. This allows list item binds to + // cached data to take immediate effect without unnecessary whizzery. + case LOAD_STATE_UNINITIALIZED: + mPlaceholder.reset(); + mProgress.reset(); + break; + case LOAD_STATE_NOT_YET_LOADED: + mPlaceholder.setPulseEnabled(true); + mPlaceholder.setVisible(true); + mProgress.setVisible(false); + break; + case LOAD_STATE_LOADING: + mPlaceholder.setVisible(false); + mProgress.setVisible(true); + break; + case LOAD_STATE_LOADED: + mPlaceholder.setVisible(false); + mProgress.setVisible(false); + break; + case LOAD_STATE_FAILED: + mPlaceholder.setPulseEnabled(false); + mPlaceholder.setVisible(true); + mProgress.setVisible(false); + break; + } + Trace.endSection(); + + mLoadState = loadState; + boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible(); + boolean progressVisible = mProgress != null && mProgress.isVisible(); + + if (DEBUG) { + Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s", + loadState, placeholderVisible, progressVisible)); + } + } + + @Override + public void invalidateDrawable(Drawable who) { + invalidateSelf(); + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + + private static class Placeholder extends TileDrawable { + + private final ValueAnimator mPulseAnimator; + private boolean mPulseEnabled = true; + private float mPulseAlphaFraction = 1f; + + public Placeholder(Drawable placeholder, Resources res, + int placeholderWidth, int placeholderHeight, int fadeOutDurationMs, + int tileColor) { + super(placeholder, placeholderWidth, placeholderHeight, tileColor, fadeOutDurationMs); + mPulseAnimator = ValueAnimator.ofInt(55, 255) + .setDuration(res.getInteger(R.integer.bitmap_placeholder_animation_duration)); + mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE); + mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE); + mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f; + setInnerAlpha(getCurrentAlpha()); + } + }); + mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + stopPulsing(); + } + }); + } + + @Override + public void setInnerAlpha(final int alpha) { + super.setInnerAlpha((int) (alpha * mPulseAlphaFraction)); + } + + public void setPulseEnabled(boolean enabled) { + mPulseEnabled = enabled; + if (!mPulseEnabled) { + stopPulsing(); + } + } + + private void stopPulsing() { + if (mPulseAnimator != null) { + mPulseAnimator.cancel(); + mPulseAlphaFraction = 1f; + setInnerAlpha(getCurrentAlpha()); + } + } + + @Override + public boolean setVisible(boolean visible) { + final boolean changed = super.setVisible(visible); + if (changed) { + if (isVisible()) { + // start + if (mPulseAnimator != null && mPulseEnabled) { + mPulseAnimator.start(); + } + } else { + // can't cancel the pulsing yet-- wait for the fade-out animation to end + // one exception: if alpha is already zero, there is no fade-out, so stop now + if (getCurrentAlpha() == 0) { + stopPulsing(); + } + } + } + return changed; + } + + } + + private static class Progress extends TileDrawable { + + private final ValueAnimator mRotateAnimator; + + public Progress(Drawable progress, Resources res, + int progressBarWidth, int progressBarHeight, int fadeOutDurationMs, + int tileColor) { + super(progress, progressBarWidth, progressBarHeight, tileColor, fadeOutDurationMs); + + mRotateAnimator = ValueAnimator.ofInt(0, 10000) + .setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration)); + mRotateAnimator.setInterpolator(new LinearInterpolator()); + mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE); + mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setLevel((Integer) animation.getAnimatedValue()); + } + }); + mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mRotateAnimator != null) { + mRotateAnimator.cancel(); + } + } + }); + } + + @Override + public boolean setVisible(boolean visible) { + final boolean changed = super.setVisible(visible); + if (changed) { + if (isVisible()) { + if (mRotateAnimator != null) { + mRotateAnimator.start(); + } + } else { + // can't cancel the rotate yet-- wait for the fade-out animation to end + // one exception: if alpha is already zero, there is no fade-out, so stop now + if (getCurrentAlpha() == 0 && mRotateAnimator != null) { + mRotateAnimator.cancel(); + } + } + } + return changed; + } + + } +} diff --git a/src/com/android/bitmap/drawable/Parallaxable.java b/src/com/android/bitmap/drawable/Parallaxable.java new file mode 100644 index 0000000..bb80e3c --- /dev/null +++ b/src/com/android/bitmap/drawable/Parallaxable.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 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.android.bitmap.drawable; + +import android.graphics.drawable.Drawable; + +/** + * {@link Drawable}s that support a parallax effect when drawing should + * implement this interface to receive the current parallax fraction to use when + * drawing. + */ +public interface Parallaxable { + /** + * @param fraction the vertical center point for the viewport, in the range [0,1] + */ + void setParallaxFraction(float fraction); +}
\ No newline at end of file diff --git a/src/com/android/bitmap/drawable/TileDrawable.java b/src/com/android/bitmap/drawable/TileDrawable.java new file mode 100644 index 0000000..40d3e16 --- /dev/null +++ b/src/com/android/bitmap/drawable/TileDrawable.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2013 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.android.bitmap.drawable; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +/** + * A drawable that wraps another drawable and places it in the center of this space. This drawable + * allows a background color for the "tile", and has a fade-out transition when + * {@link #setVisible(boolean, boolean)} indicates that it is no longer visible. + */ +public class TileDrawable extends Drawable implements Drawable.Callback { + + private final Paint mPaint = new Paint(); + private final Drawable mInner; + private final int mInnerWidth; + private final int mInnerHeight; + + protected final ValueAnimator mFadeOutAnimator; + + public TileDrawable(Drawable inner, int innerWidth, int innerHeight, + int backgroundColor, int fadeOutDurationMs) { + mInner = inner.mutate(); + mInnerWidth = innerWidth; + mInnerHeight = innerHeight; + mPaint.setColor(backgroundColor); + mInner.setCallback(this); + + mFadeOutAnimator = ValueAnimator.ofInt(255, 0) + .setDuration(fadeOutDurationMs); + mFadeOutAnimator.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setAlpha((Integer) animation.getAnimatedValue()); + } + }); + + reset(); + } + + public void reset() { + setAlpha(0); + setVisible(false); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + if (bounds.isEmpty()) { + mInner.setBounds(0, 0, 0, 0); + } else { + final int l = bounds.left + (bounds.width() / 2) - (mInnerWidth / 2); + final int t = bounds.top + (bounds.height() / 2) - (mInnerHeight / 2); + mInner.setBounds(l, t, l + mInnerWidth, t + mInnerHeight); + } + } + + @Override + public void draw(Canvas canvas) { + if (!isVisible() && mPaint.getAlpha() == 0) { + return; + } + canvas.drawRect(getBounds(), mPaint); + mInner.draw(canvas); + } + + @Override + public void setAlpha(int alpha) { + final int old = mPaint.getAlpha(); + mPaint.setAlpha(alpha); + setInnerAlpha(alpha); + if (alpha != old) { + invalidateSelf(); + } + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + mInner.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return 0; + } + + protected int getCurrentAlpha() { + return mPaint.getAlpha(); + } + + public boolean setVisible(boolean visible) { + return setVisible(visible, true /* dontcare */); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + mInner.setVisible(visible, restart); + final boolean changed = super.setVisible(visible, restart); + if (changed) { + if (isVisible()) { + // pop in (no-op) + // the transition will still be smooth if the previous state's layer fades out + mFadeOutAnimator.cancel(); + setAlpha(255); + } else { + // fade out + if (mPaint.getAlpha() == 255 && !getBounds().isEmpty()) { + mFadeOutAnimator.start(); + } + } + } + return changed; + } + + @Override + protected boolean onLevelChange(int level) { + return mInner.setLevel(level); + } + + /** + * Changes the alpha on just the inner wrapped drawable. + */ + public void setInnerAlpha(int alpha) { + mInner.setAlpha(alpha); + } + + @Override + public void invalidateDrawable(Drawable who) { + invalidateSelf(); + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + +} diff --git a/src/com/android/bitmap/util/BitmapUtils.java b/src/com/android/bitmap/util/BitmapUtils.java new file mode 100644 index 0000000..094d771 --- /dev/null +++ b/src/com/android/bitmap/util/BitmapUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2013 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.android.bitmap.util; + +import android.graphics.Rect; + +public abstract class BitmapUtils { + + /** + * Calculate a center-crop rectangle for the given input and output + * parameters. The output rectangle to use is written in the given outRect. + * + * @param srcW the source width + * @param srcH the source height + * @param dstW the destination width + * @param dstH the destination height + * @param dstSliceH the height extent (in destination coordinates) to + * exclude when cropping. You would typically pass dstH, unless + * you are trying to normalize different items to the same + * vertical crop range. + * @param sampleSize a scaling factor that rect calculation will only use if + * it's more aggressive than regular scaling + * @param vertSliceFrac vertical slice fraction determines the vertical + * center point for the crop rect. Range is from [0.0, 1.0]. To + * perform a vertically centered crop, use 0.5. Otherwise, see + * absoluteFrac. + * @param absoluteFrac determines how the vertSliceFrac affects the vertical + * center point. If this parameter is true, the vertical center + * of the resulting output rectangle will be exactly + * [vertSliceFrac * srcH], with care taken to keep the bounds + * within the source rectangle. If this parameter is false, the + * vertical center will be calculated so that the values of + * vertSliceFrac from 0.0 to 1.0 will linearly cover the entirety + * of the source rectangle. + * @param verticalMultiplier an optional multiplier that will alter the + * output Rect's aspect ratio to be this much taller in the event + * that y is the limiting dimension + * @param outRect a Rect to write the resulting crop coordinates into + */ + public static void calculateCroppedSrcRect(final int srcW, final int srcH, final int dstW, + final int dstH, final int dstSliceH, int sampleSize, final float vertSliceFrac, + final boolean absoluteFrac, final float verticalMultiplier, final Rect outRect) { + if (sampleSize < 1) { + sampleSize = 1; + } + final float regularScale; + final float wScale = (float) srcW / dstW; + final float hScale = (float) srcH / dstH; + if (hScale < wScale) { + regularScale = hScale / verticalMultiplier; + } else { + regularScale = wScale; + } + + final float scale = Math.min(sampleSize, regularScale); + + final int srcCroppedW = Math.round(dstW * scale); + final int srcCroppedH = Math.round(dstH * scale); + final int srcCroppedSliceH = Math.round(dstSliceH * scale); + final int srcHalfSliceH = Math.min(srcCroppedSliceH, srcH) / 2; + + outRect.left = (srcW - srcCroppedW) / 2; + outRect.right = outRect.left + srcCroppedW; + + final int centerV; + if (absoluteFrac) { + final int minCenterV = srcHalfSliceH; + final int maxCenterV = srcH - srcHalfSliceH; + centerV = Math.max(minCenterV, Math.min(maxCenterV, Math.round(srcH * vertSliceFrac))); + } else { + centerV = Math + .round(Math.abs(srcH - srcCroppedSliceH) * vertSliceFrac + srcHalfSliceH); + } + + outRect.top = centerV - srcCroppedH / 2; + outRect.bottom = outRect.top + srcCroppedH; + } + + /** + * @param srcW + * @param srcH + * @param dstW + * @param dstH + * @param outRect + */ + public static void calculateCroppedSrcRect(int srcW, int srcH, int dstW, int dstH, + Rect outRect) { + calculateCroppedSrcRect(srcW, srcH, dstW, dstH, Integer.MAX_VALUE, outRect); + } + + public static void calculateCroppedSrcRect(int srcW, int srcH, int dstW, int dstH, + int sampleSize, Rect outRect) { + if (sampleSize < 1) { + sampleSize = 1; + } + final float regularScale = Math.min( + (float) srcW / dstW, + (float) srcH / dstH); + + final float scale = Math.min(sampleSize, regularScale); + + final int srcCroppedW = Math.round(dstW * scale); + final int srcCroppedH = Math.round(dstH * scale); + + outRect.left = (srcW - srcCroppedW) / 2; + outRect.right = outRect.left + srcCroppedW; + + outRect.top = (srcH - srcCroppedH) / 2; + outRect.bottom = outRect.top + srcCroppedH; + } +} diff --git a/src/com/android/bitmap/util/Exif.java b/src/com/android/bitmap/util/Exif.java new file mode 100644 index 0000000..de7700b --- /dev/null +++ b/src/com/android/bitmap/util/Exif.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2013 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.android.bitmap.util; + +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * TODO + * Exif and InputStreamBuffer were pulled in from frameworks/ex/photo, and should be part of a + * separate library that is used by both this and chips. + */ +public class Exif { + private static final String TAG = Exif.class.getSimpleName(); + + /** + * Returns the degrees in clockwise. Values are 0, 90, 180, or 270. + * @param inputStream The input stream will not be closed for you. + * @param byteSize Recommended parameter declaring the length of the input stream. If you + * pass in -1, we will have to read more from the input stream. + * @return 0, 90, 180, or 270. + */ + public static int getOrientation(final InputStream inputStream, final long byteSize) { + if (inputStream == null) { + return 0; + } + + /* + Looking at this algorithm, we never look ahead more than 8 bytes. As long as we call + advanceTo() at the end of every loop, we should never have to reallocate a larger buffer. + + Also, the most we ever read backwards is 4 bytes. pack() reads backwards if the encoding + is in little endian format. These following two lines potentially reads 4 bytes backwards: + + int tag = pack(jpeg, offset, 4, false); + count = pack(jpeg, offset - 2, 2, littleEndian); + + To be safe, we will always advance to some index-4, so we'll need 4 more for the +8 + look ahead, which makes it a +12 look ahead total. Use 16 just in case my analysis is off. + + This means we only need to allocate a single 16 byte buffer. + + Note: If you do not pass in byteSize parameter, a single large allocation will occur. + For a 1MB image, I see one 30KB allocation. This is due to the line containing: + + has(jpeg, byteSize, offset + length - 1) + + where length is a variable int (around 30KB above) read from the EXIF headers. + + This is still much better than allocating a 1MB byte[] which we were doing before. + */ + + final int lookAhead = 16; + final int readBackwards = 4; + final InputStreamBuffer jpeg = new InputStreamBuffer(inputStream, lookAhead, false); + + int offset = 0; + int length = 0; + + if (has(jpeg, byteSize, 1)) { + // JPEG image files begin with FF D8. Only JPEG images have EXIF data. + final boolean possibleJpegFormat = jpeg.get(0) == (byte) 0xFF + && jpeg.get(1) == (byte) 0xD8; + if (!possibleJpegFormat) { + return 0; + } + } + + // ISO/IEC 10918-1:1993(E) + while (has(jpeg, byteSize, offset + 3) && (jpeg.get(offset++) & 0xFF) == 0xFF) { + final int marker = jpeg.get(offset) & 0xFF; + + // Check if the marker is a padding. + if (marker == 0xFF) { + continue; + } + offset++; + + // Check if the marker is SOI or TEM. + if (marker == 0xD8 || marker == 0x01) { + continue; + } + // Check if the marker is EOI or SOS. + if (marker == 0xD9 || marker == 0xDA) { + // Loop ends. + jpeg.advanceTo(offset - readBackwards); + break; + } + + // Get the length and check if it is reasonable. + length = pack(jpeg, offset, 2, false); + if (length < 2 || !has(jpeg, byteSize, offset + length - 1)) { + Log.e(TAG, "Invalid length"); + return 0; + } + + // Break if the marker is EXIF in APP1. + if (marker == 0xE1 && length >= 8 && + pack(jpeg, offset + 2, 4, false) == 0x45786966 && + pack(jpeg, offset + 6, 2, false) == 0) { + offset += 8; + length -= 8; + // Loop ends. + jpeg.advanceTo(offset - readBackwards); + break; + } + + // Skip other markers. + offset += length; + length = 0; + + // Loop ends. + jpeg.advanceTo(offset - readBackwards); + } + + // JEITA CP-3451 Exif Version 2.2 + if (length > 8) { + // Identify the byte order. + int tag = pack(jpeg, offset, 4, false); + if (tag != 0x49492A00 && tag != 0x4D4D002A) { + Log.e(TAG, "Invalid byte order"); + return 0; + } + final boolean littleEndian = (tag == 0x49492A00); + + // Get the offset and check if it is reasonable. + int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; + if (count < 10 || count > length) { + Log.e(TAG, "Invalid offset"); + return 0; + } + offset += count; + length -= count; + + // Offset has changed significantly. + jpeg.advanceTo(offset - readBackwards); + + // Get the count and go through all the elements. + count = pack(jpeg, offset - 2, 2, littleEndian); + + while (count-- > 0 && length >= 12) { + // Get the tag and check if it is orientation. + tag = pack(jpeg, offset, 2, littleEndian); + if (tag == 0x0112) { + // We do not really care about type and count, do we? + final int orientation = pack(jpeg, offset + 8, 2, littleEndian); + switch (orientation) { + case 1: + return 0; + case 3: + return 180; + case 6: + return 90; + case 8: + return 270; + } + Log.i(TAG, "Unsupported orientation"); + return 0; + } + offset += 12; + length -= 12; + + // Loop ends. + jpeg.advanceTo(offset - readBackwards); + } + } + + return 0; + } + + private static int pack(final InputStreamBuffer bytes, int offset, int length, + final boolean littleEndian) { + int step = 1; + if (littleEndian) { + offset += length - 1; + step = -1; + } + + int value = 0; + while (length-- > 0) { + value = (value << 8) | (bytes.get(offset) & 0xFF); + offset += step; + } + return value; + } + + private static boolean has(final InputStreamBuffer jpeg, final long byteSize, final int index) { + if (byteSize >= 0) { + return index < byteSize; + } else { + // For large values of index, this will cause the internal buffer to resize. + return jpeg.has(index); + } + } + + @Deprecated + public static int getOrientation(final byte[] jpeg) { + return getOrientation(new ByteArrayInputStream(jpeg), jpeg.length); + } +}
\ No newline at end of file diff --git a/src/com/android/bitmap/util/InputStreamBuffer.java b/src/com/android/bitmap/util/InputStreamBuffer.java new file mode 100644 index 0000000..c1c1547 --- /dev/null +++ b/src/com/android/bitmap/util/InputStreamBuffer.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2013 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.android.bitmap.util; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +/** + * Wrapper for {@link InputStream} that allows you to read bytes from it like a byte[]. An + * internal buffer is kept as small as possible to avoid large unnecessary allocations. + * + * <p/> + * Care must be taken so that the internal buffer is kept small. The best practice is to + * precalculate the maximum buffer size that you will need. For example, + * say you have a loop that reads bytes from index <code>0</code> to <code>10</code>, + * skips to index <code>N</code>, reads from index <code>N</code> to <code>N+10</code>, etc. Then + * you know that the internal buffer can have a maximum size of <code>10</code>, + * and you should set the <code>bufferSize</code> parameter to <code>10</code> in the constructor. + * + * <p/> + * Use {@link #advanceTo(int)} to declare that you will not need to access lesser indexes. This + * helps to keep the internal buffer small. In the above example, after reading bytes from index + * <code>0</code> to <code>10</code>, you should call <code>advanceTo(N)</code> so that internal + * buffer becomes filled with bytes from index <code>N</code> to <code>N+10</code>. + * + * <p/> + * If you know that you are reading bytes from a <strong>strictly</strong> increasing or equal + * index, then you should set the <code>autoAdvance</code> parameter to <code>true</code> in the + * constructor. For complicated access patterns, or when you prefer to control the internal + * buffer yourself, set <code>autoAdvance</code> to <code>false</code>. When + * <code>autoAdvance</code> is enabled, every time an index is beyond the buffer length, + * the buffer will be shifted forward such that the index requested becomes the first element in + * the buffer. + * + * <p/> + * All public methods with parameter <code>index</code> are absolute indexed. The index is from + * the beginning of the wrapped input stream. + */ +public class InputStreamBuffer { + + private static final boolean DEBUG = false; + private static final int DEBUG_MAX_BUFFER_SIZE = 80; + private static final String TAG = InputStreamBuffer.class.getSimpleName(); + + private InputStream mInputStream; + private byte[] mBuffer; + private boolean mAutoAdvance; + /** Byte count the buffer is offset by. */ + private int mOffset = 0; + /** Number of bytes filled in the buffer. */ + private int mFilled = 0; + + /** + * Construct a new wrapper for an InputStream. + * + * <p/> + * If <code>autoAdvance</code> is true, behavior is undefined if you call {@link #get(int)} + * or {@link #has(int)} with an index N, then some arbitrary time later call {@link #get(int)} + * or {@link #has(int)} with an index M < N. The wrapper may return the right value, + * if the buffer happens to still contain index M, but more likely it will throw an + * {@link IllegalStateException}. + * + * <p/> + * If <code>autoAdvance</code> is false, you must be diligent and call {@link #advanceTo(int)} + * at the appropriate times to ensure that the internal buffer is not unnecessarily resized + * and reallocated. + * + * @param inputStream The input stream to wrap. The input stream will not be closed by the + * wrapper. + * @param bufferSize The initial size for the internal buffer. The buffer size should be + * carefully chosen to avoid resizing and reallocating the internal buffer. + * The internal buffer size used will be the least power of two greater + * than this parameter. + * @param autoAdvance Determines the behavior when you need to read an index that is beyond + * the internal buffer size. If true, the internal buffer will shift so + * that the requested index becomes the first element. If false, + * the internal buffer size will grow to the smallest power of 2 which is + * greater than the requested index. + */ + public InputStreamBuffer(final InputStream inputStream, int bufferSize, + final boolean autoAdvance) { + mInputStream = inputStream; + if (bufferSize <= 0) { + throw new IllegalArgumentException( + String.format("Buffer size %d must be positive.", bufferSize)); + } + bufferSize = leastPowerOf2(bufferSize); + mBuffer = new byte[bufferSize]; + mAutoAdvance = autoAdvance; + } + + /** + * Attempt to get byte at the requested index from the wrapped input stream. If the internal + * buffer contains the requested index, return immediately. If the index is less than the + * head of the buffer, or the index is greater or equal to the size of the wrapped input stream, + * a runtime exception is thrown. + * + * <p/> + * If the index is not in the internal buffer, but it can be requested from the input stream, + * {@link #fill(int)} will be called first, and the byte at the index returned. + * + * <p/> + * You should always call {@link #has(int)} with the same index, unless you are sure that no + * exceptions will be thrown as described above. + * + * <p/> + * Consider calling {@link #advanceTo(int)} if you know that you will never request a lesser + * index in the future. + * @param index The requested index. + * @return The byte at that index. + */ + public byte get(final int index) throws IllegalStateException, IndexOutOfBoundsException { + Trace.beginSection("get"); + if (has(index)) { + final int i = index - mOffset; + Trace.endSection(); + return mBuffer[i]; + } else { + Trace.endSection(); + throw new IndexOutOfBoundsException( + String.format("Index %d beyond length.", index)); + } + } + + /** + * Attempt to return whether the requested index is within the size of the wrapped input + * stream. One side effect is {@link #fill(int)} will be called. + * + * <p/> + * If this method returns true, it is guaranteed that {@link #get(int)} with the same index + * will not fail. That means that if the requested index is within the size of the wrapped + * input stream, but the index is less than the head of the internal buffer, + * a runtime exception is thrown. + * + * <p/> + * See {@link #get(int)} for caveats. A lot of the same warnings about exceptions and + * <code>advanceTo()</code> apply. + * @param index The requested index. + * @return True if requested index is within the size of the wrapped input stream. False if + * the index is beyond the size. + */ + public boolean has(final int index) throws IllegalStateException, IndexOutOfBoundsException { + Trace.beginSection("has"); + if (index < mOffset) { + Trace.endSection(); + throw new IllegalStateException( + String.format("Index %d is before buffer %d", index, mOffset)); + } + + final int i = index - mOffset; + + // Requested index not in internal buffer. + if (i >= mFilled || i >= mBuffer.length) { + Trace.endSection(); + return fill(index); + } + + Trace.endSection(); + return true; + } + + /** + * Attempts to advance the head of the buffer to the requested index. If the index is less + * than the head of the buffer, the internal state will not be changed. + * + * <p/> + * Advancing does not fill the internal buffer. The next {@link #get(int)} or + * {@link #has(int)} call will fill the buffer. + */ + public void advanceTo(final int index) throws IllegalStateException, IndexOutOfBoundsException { + Trace.beginSection("advance to"); + final int i = index - mOffset; + if (i <= 0) { + // noop + Trace.endSection(); + return; + } else if (i < mFilled) { + // Shift elements starting at i to position 0. + shiftToBeginning(i); + mOffset = index; + mFilled = mFilled - i; + } else if (mInputStream != null) { + // Burn some bytes from the input stream to match the new index. + int burn = i - mFilled; + boolean empty = false; + int fails = 0; + try { + while (burn > 0) { + final long burned = mInputStream.skip(burn); + if (burned <= 0) { + fails++; + } else { + burn -= burned; + } + + if (fails >= 5) { + empty = true; + break; + } + } + } catch (IOException ignored) { + empty = true; + } + + if (empty) { + //Mark input stream as consumed. + mInputStream = null; + } + + mOffset = index - burn; + mFilled = 0; + } else { + // Advancing beyond the input stream. + mOffset = index; + mFilled = 0; + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, String.format("advanceTo %d buffer: %s", i, this)); + } + Trace.endSection(); + } + + /** + * Attempt to fill the internal buffer fully. The buffer will be modified such that the + * requested index will always be in the buffer. If the index is less + * than the head of the buffer, a runtime exception is thrown. + * + * <p/> + * If the requested index is already in bounds of the buffer, then the buffer will just be + * filled. + * + * <p/> + * Otherwise, if <code>autoAdvance</code> was set to true in the constructor, + * {@link #advanceTo(int)} will be called with the requested index, + * and then the buffer filled. If <code>autoAdvance</code> was set to false, + * we allocate a single larger buffer of a least multiple-of-two size that can contain the + * requested index. The elements in the old buffer are copied over to the head of the new + * buffer. Then the entire buffer is filled. + * @param index The requested index. + * @return True if the byte at the requested index has been filled. False if the wrapped + * input stream ends before we reach the index. + */ + private boolean fill(final int index) { + Trace.beginSection("fill"); + if (index < mOffset) { + Trace.endSection(); + throw new IllegalStateException( + String.format("Index %d is before buffer %d", index, mOffset)); + } + + int i = index - mOffset; + // Can't fill buffer anymore if input stream is consumed. + if (mInputStream == null) { + Trace.endSection(); + return false; + } + + // Increase buffer size if necessary. + int length = i + 1; + if (length > mBuffer.length) { + if (mAutoAdvance) { + advanceTo(index); + i = index - mOffset; + } else { + length = leastPowerOf2(length); + Log.w(TAG, String.format( + "Increasing buffer length from %d to %d. Bad buffer size chosen, " + + "or advanceTo() not called.", + mBuffer.length, length)); + mBuffer = Arrays.copyOf(mBuffer, length); + } + } + + // Read from input stream to fill buffer. + int read = -1; + try { + read = mInputStream.read(mBuffer, mFilled, mBuffer.length - mFilled); + } catch (IOException ignored) { + } + + if (read != -1) { + mFilled = mFilled + read; + } else { + // Mark input stream as consumed. + mInputStream = null; + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, String.format("fill %d buffer: %s", i, this)); + } + + Trace.endSection(); + return i < mFilled; + } + + /** + * Modify the internal buffer so that all the bytes are shifted towards the head by + * <code>i</code>. In other words, the byte at index <code>i</code> will now be at index + * <code>0</code>. Bytes from a lesser index are tossed. + * @param i How much to shift left. + */ + private void shiftToBeginning(final int i) { + if (i >= mBuffer.length) { + throw new IndexOutOfBoundsException( + String.format("Index %d out of bounds. Length %d", i, mBuffer.length)); + } + for (int j = 0; j + i < mFilled; j++) { + mBuffer[j] = mBuffer[j + i]; + } + } + + @Override + public String toString() { + if (DEBUG) { + return toDebugString(); + } + return String.format("+%d+%d [%d]", mOffset, mBuffer.length, mFilled); + } + + public String toDebugString() { + Trace.beginSection("to debug string"); + final StringBuilder sb = new StringBuilder(); + sb.append("+").append(mOffset); + sb.append("+").append(mBuffer.length); + sb.append(" ["); + for (int i = 0; i < mBuffer.length && i < DEBUG_MAX_BUFFER_SIZE; i++) { + if (i > 0) { + sb.append(","); + } + if (i < mFilled) { + sb.append(String.format("%02X", mBuffer[i])); + } else { + sb.append("__"); + } + } + if (mInputStream != null) { + sb.append("..."); + } + sb.append("]"); + + Trace.endSection(); + return sb.toString(); + } + + /** + * Calculate the least power of two greater than or equal to the input. + */ + private static int leastPowerOf2(int n) { + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + n++; + return n; + } +}
\ No newline at end of file diff --git a/src/com/android/bitmap/util/RectUtils.java b/src/com/android/bitmap/util/RectUtils.java new file mode 100644 index 0000000..70eb0de --- /dev/null +++ b/src/com/android/bitmap/util/RectUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2013 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.android.bitmap.util; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; + +public class RectUtils { + + /** + * Transform the upright full rectangle so that it bounds the original rotated image, + * given by the orientation. Transform the upright partial rectangle such that it would apply + * to the same region of the transformed full rectangle. + * + * The top-left of the transformed full rectangle will always be placed at (0, 0). + * @param orientation The exif orientation (0, 90, 180, 270) of the original image. The + * transformed full and partial rectangles will be in this orientation's + * coordinate space. + * @param fullRect The upright full rectangle. This rectangle will be modified. + * @param partialRect The upright partial rectangle. This rectangle will be modified. + */ + public static void rotateRectForOrientation(final int orientation, final Rect fullRect, + final Rect partialRect) { + final Matrix matrix = new Matrix(); + // Exif orientation specifies how the camera is rotated relative to the actual subject. + // First rotate in the opposite direction. + matrix.setRotate(-orientation); + final RectF fullRectF = new RectF(fullRect); + final RectF partialRectF = new RectF(partialRect); + matrix.mapRect(fullRectF); + matrix.mapRect(partialRectF); + // Then translate so that the upper left corner of the rotated full rect is at (0,0). + matrix.reset(); + matrix.setTranslate(-fullRectF.left, -fullRectF.top); + matrix.mapRect(fullRectF); + matrix.mapRect(partialRectF); + // Orientation transformation is complete. + fullRect.set((int) fullRectF.left, (int) fullRectF.top, (int) fullRectF.right, + (int) fullRectF.bottom); + partialRect.set((int) partialRectF.left, (int) partialRectF.top, (int) partialRectF.right, + (int) partialRectF.bottom); + } + + public static void rotateRect(final int degrees, final int px, final int py, final Rect rect) { + final RectF rectF = new RectF(rect); + final Matrix matrix = new Matrix(); + matrix.setRotate(degrees, px, py); + matrix.mapRect(rectF); + rect.set((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom); + } +} diff --git a/src/com/android/bitmap/util/Trace.java b/src/com/android/bitmap/util/Trace.java new file mode 100644 index 0000000..e303d72 --- /dev/null +++ b/src/com/android/bitmap/util/Trace.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 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.android.bitmap.util; + +import java.lang.reflect.Method; + +public class Trace { + + private static Method sBegin; + private static Method sEnd; + + public static void init() { + if (sBegin != null && sEnd != null) { + return; + } + try { + final Class<?> cls = Class.forName("android.os.Trace"); + sBegin = cls.getMethod("beginSection", String.class); + sEnd = cls.getMethod("endSection"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void beginSection(String tag) { + if (sBegin == null) { + return; + } + try { + sBegin.invoke(null, tag); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void endSection() { + if (sEnd == null) { + return; + } + try { + sEnd.invoke(null, (Object[]) null); + } catch (Exception e) { + e.printStackTrace(); + } + } +} |