summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Blitzstein <sblitz@google.com>2013-10-09 14:11:27 -0700
committerSam Blitzstein <sblitz@google.com>2013-10-15 17:34:58 -0700
commit93a35b93dc582e38ff8ee5979754a16b4bf4da0c (patch)
tree9034ab3155e8781b0cd77fb70882f911080f6f89
parentce2b0fdc1e9c9d083faab75b6bdfbea27bf574e2 (diff)
downloadbitmap-93a35b93dc582e38ff8ee5979754a16b4bf4da0c.tar.gz
Initial commit from Gmail's Cache system.
Change-Id: I14168ab3bc02b77399a1812f62bd77ac797232c5
-rw-r--r--Android.mk28
-rw-r--r--AndroidManifest.xml26
-rw-r--r--res/values/colors.xml20
-rw-r--r--res/values/constants.xml26
-rw-r--r--res/values/dimens.xml23
-rw-r--r--src/com/android/bitmap/BitmapCache.java28
-rw-r--r--src/com/android/bitmap/ContiguousFIFOAggregator.java313
-rw-r--r--src/com/android/bitmap/DecodeAggregator.java26
-rw-r--r--src/com/android/bitmap/DecodeTask.java498
-rw-r--r--src/com/android/bitmap/Poolable.java24
-rw-r--r--src/com/android/bitmap/PooledCache.java37
-rw-r--r--src/com/android/bitmap/ReusableBitmap.java151
-rw-r--r--src/com/android/bitmap/UnrefedBitmapCache.java140
-rw-r--r--src/com/android/bitmap/UnrefedPooledCache.java231
-rw-r--r--src/com/android/bitmap/drawable/BasicBitmapDrawable.java276
-rw-r--r--src/com/android/bitmap/drawable/BitmapRequestKey.java60
-rw-r--r--src/com/android/bitmap/drawable/ExtendedBitmapDrawable.java579
-rw-r--r--src/com/android/bitmap/drawable/Parallaxable.java31
-rw-r--r--src/com/android/bitmap/drawable/TileDrawable.java164
-rw-r--r--src/com/android/bitmap/util/BitmapUtils.java125
-rw-r--r--src/com/android/bitmap/util/Exif.java216
-rw-r--r--src/com/android/bitmap/util/InputStreamBuffer.java376
-rw-r--r--src/com/android/bitmap/util/RectUtils.java66
-rw-r--r--src/com/android/bitmap/util/Trace.java60
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();
+ }
+ }
+}