Merge "Import translations. DO NOT MERGE" into gb-ub-photos-arches
diff --git a/Android.mk b/Android.mk
index b0ebf89..4854a5c 100644
--- a/Android.mk
+++ b/Android.mk
@@ -18,7 +18,7 @@
 
 LOCAL_OVERRIDES_PACKAGES := Gallery Gallery3D GalleryNew3D
 
-LOCAL_SDK_VERSION := current
+LOCAL_SDK_VERSION := 16
 
 LOCAL_JNI_SHARED_LIBRARIES := libjni_mosaic libjni_eglfence
 
diff --git a/gallerycommon/Android.mk b/gallerycommon/Android.mk
index fb81dd9..1d34147 100644
--- a/gallerycommon/Android.mk
+++ b/gallerycommon/Android.mk
@@ -22,6 +22,6 @@
 
 LOCAL_MODULE := com.android.gallery3d.common2
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_SDK_VERSION := current
+LOCAL_SDK_VERSION := 16
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
index 85b0925..620e1ae 100644
--- a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
+++ b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
@@ -16,10 +16,17 @@
 
 package com.android.gallery3d.common;
 
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.hardware.Camera;
+import android.hardware.Camera.FaceDetectionListener;
 import android.os.Build;
 import android.provider.MediaStore.MediaColumns;
 import android.view.View;
 
+import java.lang.reflect.Field;
+
 public class ApiHelper {
     public static interface VERSION_CODES {
         // These value are copied from Build.VERSION_CODES
@@ -44,6 +51,63 @@
     public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER =
             Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
 
+    public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_FACTORY =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_SET_BEAM_PUSH_URIS =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_SET_DEFALT_BUFFER_SIZE = hasMethod(
+            "android.graphics.SurfaceTexture", "setDefaultBufferSize",
+            int.class, int.class);
+
+    public static final boolean HAS_RELEASE_SURFACE_TEXTURE = hasMethod(
+            "android.graphics.SurfaceTexture", "release");
+
+    public static final boolean HAS_MTP =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1;
+
+    public static final boolean HAS_AUTO_FOCUS_MOVE_CALLBACK =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static final boolean HAS_ACTIVITY_INVALIDATE_OPTIONS_MENU =
+            hasMethod(Activity.class, "invalidateOptionsMenu");
+
+    public static final boolean HAS_REMOTE_VIEWS_SERVICE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_INTENT_EXTRA_LOCAL_ONLY =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
+    public static final boolean HAS_SET_SYSTEM_UI_VISIBILITY =
+            hasMethod(View.class, "setSystemUiVisibility", int.class);
+
+    public static final boolean HAS_FACE_DETECTION =
+            hasClass(Camera.class, "android.hardware.Camera$FaceDetectionListener") &&
+            hasMethod(Camera.class, "setFaceDetectionListener", FaceDetectionListener.class) &&
+            hasMethod(Camera.class, "startFaceDetection") &&
+            hasMethod(Camera.class, "stopFaceDetection") &&
+            hasMethod(Camera.Parameters.class, "getMaxNumDetectedFaces");
+
+    public static final boolean HAS_GET_CAMERA_DISABLED =
+            hasMethod(DevicePolicyManager.class, "getCameraDisabled", ComponentName.class);
+
+    public static final boolean HAS_MEDIA_ACTION_SOUND =
+            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+
+    public static int getIntFieldIfExists(Class<?> klass, String fieldName,
+            Class<?> obj, int defaultVal) {
+        try {
+            Field f = klass.getDeclaredField(fieldName);
+            return f.getInt(obj);
+        } catch (Exception e) {
+            return defaultVal;
+        }
+    }
+
+    public static final boolean HAS_SET_ICON_ATTRIBUTE =
+            Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB;
+
     private static boolean hasField(Class<?> klass, String fieldName) {
         try {
             klass.getDeclaredField(fieldName);
@@ -52,4 +116,36 @@
             return false;
         }
     }
+
+    private static boolean hasMethod(String className, String methodName,
+            Class<?>... parameterTypes) {
+        try {
+            Class<?> klass = Class.forName(className);
+            klass.getDeclaredMethod(methodName, parameterTypes);
+            return true;
+        } catch (Throwable th) {
+            return false;
+        }
+    }
+
+    private static boolean hasMethod(
+            Class<?> klass, String methodName, Class<?> ... paramTypes) {
+        try {
+            klass.getDeclaredMethod(methodName, paramTypes);
+            return true;
+        } catch (NoSuchMethodException e) {
+            return false;
+        }
+    }
+
+    private static boolean hasClass(Class<?> klass, String className) {
+        Class<?>[] klasses = klass.getClasses();
+        for (int i = 0; i < klasses.length; ++i) {
+            if (klasses[i].getName().equals(className)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
 }
diff --git a/gallerycommon/src/com/android/gallery3d/common/ExifTags.java b/gallerycommon/src/com/android/gallery3d/common/ExifTags.java
new file mode 100644
index 0000000..9b11fe4
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/ExifTags.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2012 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.gallery3d.common;
+
+/**
+ * The class holds the EXIF tag names that are not available in
+ * {@link android.media.ExifInterface} prior to API level 11.
+ */
+public interface ExifTags {
+    static final String TAG_ISO = "ISOSpeedRatings";
+    static final String TAG_EXPOSURE_TIME = "ExposureTime";
+    static final String TAG_APERTURE = "FNumber";
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/OverScroller.java b/gallerycommon/src/com/android/gallery3d/common/OverScroller.java
new file mode 100644
index 0000000..1ab7953
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/OverScroller.java
@@ -0,0 +1,958 @@
+/*
+ * Copyright (C) 2010 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.gallery3d.common;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.util.FloatMath;
+import android.util.Log;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+/**
+ * This class encapsulates scrolling with the ability to overshoot the bounds
+ * of a scrolling operation. This class is a drop-in replacement for
+ * {@link android.widget.Scroller} in most cases.
+ */
+public class OverScroller {
+    private int mMode;
+
+    private final SplineOverScroller mScrollerX;
+    private final SplineOverScroller mScrollerY;
+
+    private Interpolator mInterpolator;
+
+    private final boolean mFlywheel;
+
+    private static final int DEFAULT_DURATION = 250;
+    private static final int SCROLL_MODE = 0;
+    private static final int FLING_MODE = 1;
+
+    /**
+     * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel.
+     * @param context
+     */
+    public OverScroller(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Creates an OverScroller with flywheel enabled.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     */
+    public OverScroller(Context context, Interpolator interpolator) {
+        this(context, interpolator, true);
+    }
+
+    /**
+     * Creates an OverScroller.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
+     * @hide
+     */
+    public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
+        mInterpolator = interpolator;
+        mFlywheel = flywheel;
+        mScrollerX = new SplineOverScroller();
+        mScrollerY = new SplineOverScroller();
+
+        SplineOverScroller.initFromContext(context);
+    }
+
+    /**
+     * Creates an OverScroller with flywheel enabled.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+     * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+     * means no bounce. This behavior is no longer supported and this coefficient has no effect.
+     * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
+     * behavior is no longer supported and this coefficient has no effect.
+     * !deprecated Use {!link #OverScroller(Context, Interpolator, boolean)} instead.
+     */
+    public OverScroller(Context context, Interpolator interpolator,
+            float bounceCoefficientX, float bounceCoefficientY) {
+        this(context, interpolator, true);
+    }
+
+    /**
+     * Creates an OverScroller.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+     * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+     * means no bounce. This behavior is no longer supported and this coefficient has no effect.
+     * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
+     * behavior is no longer supported and this coefficient has no effect.
+     * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
+     * !deprecated Use {!link OverScroller(Context, Interpolator, boolean)} instead.
+     */
+    public OverScroller(Context context, Interpolator interpolator,
+            float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
+        this(context, interpolator, flywheel);
+    }
+
+    void setInterpolator(Interpolator interpolator) {
+        mInterpolator = interpolator;
+    }
+
+    /**
+     * The amount of friction applied to flings. The default value
+     * is {@link ViewConfiguration#getScrollFriction}.
+     *
+     * @param friction A scalar dimension-less value representing the coefficient of
+     *         friction.
+     */
+    public final void setFriction(float friction) {
+        mScrollerX.setFriction(friction);
+        mScrollerY.setFriction(friction);
+    }
+
+    /**
+     *
+     * Returns whether the scroller has finished scrolling.
+     *
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mScrollerX.mFinished && mScrollerY.mFinished;
+    }
+
+    /**
+     * Force the finished field to a particular value. Contrary to
+     * {@link #abortAnimation()}, forcing the animation to finished
+     * does NOT cause the scroller to move to the final x and y
+     * position.
+     *
+     * @param finished The new finished value.
+     */
+    public final void forceFinished(boolean finished) {
+        mScrollerX.mFinished = mScrollerY.mFinished = finished;
+    }
+
+    /**
+     * Returns the current X offset in the scroll.
+     *
+     * @return The new X offset as an absolute distance from the origin.
+     */
+    public final int getCurrX() {
+        return mScrollerX.mCurrentPosition;
+    }
+
+    /**
+     * Returns the current Y offset in the scroll.
+     *
+     * @return The new Y offset as an absolute distance from the origin.
+     */
+    public final int getCurrY() {
+        return mScrollerY.mCurrentPosition;
+    }
+
+    /**
+     * Returns the absolute value of the current velocity.
+     *
+     * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
+     */
+    public float getCurrVelocity() {
+        float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity;
+        squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity;
+        return FloatMath.sqrt(squaredNorm);
+    }
+
+    /**
+     * Returns the start X offset in the scroll.
+     *
+     * @return The start X offset as an absolute distance from the origin.
+     */
+    public final int getStartX() {
+        return mScrollerX.mStart;
+    }
+
+    /**
+     * Returns the start Y offset in the scroll.
+     *
+     * @return The start Y offset as an absolute distance from the origin.
+     */
+    public final int getStartY() {
+        return mScrollerY.mStart;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final X offset as an absolute distance from the origin.
+     */
+    public final int getFinalX() {
+        return mScrollerX.mFinal;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final Y offset as an absolute distance from the origin.
+     */
+    public final int getFinalY() {
+        return mScrollerY.mFinal;
+    }
+
+    /**
+     * Returns how long the scroll event will take, in milliseconds.
+     *
+     * @return The duration of the scroll in milliseconds.
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScrollers don't necessarily have a fixed duration.
+     *             This function will lie to the best of its ability.
+     */
+    @Deprecated
+    public final int getDuration() {
+        return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
+    }
+
+    /**
+     * Extend the scroll animation. This allows a running animation to scroll
+     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+     *
+     * @param extend Additional time to scroll in milliseconds.
+     * @see #setFinalX(int)
+     * @see #setFinalY(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScrollers don't necessarily have a fixed duration.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void extendDuration(int extend) {
+        mScrollerX.extendDuration(extend);
+        mScrollerY.extendDuration(extend);
+    }
+
+    /**
+     * Sets the final position (X) for this scroller.
+     *
+     * @param newX The new X offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalY(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScroller's final position may change during an animation.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void setFinalX(int newX) {
+        mScrollerX.setFinalPosition(newX);
+    }
+
+    /**
+     * Sets the final position (Y) for this scroller.
+     *
+     * @param newY The new Y offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalX(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScroller's final position may change during an animation.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void setFinalY(int newY) {
+        mScrollerY.setFinalPosition(newY);
+    }
+
+    /**
+     * Call this when you want to know the new location. If it returns true, the
+     * animation is not yet finished.
+     */
+    public boolean computeScrollOffset() {
+        if (isFinished()) {
+            return false;
+        }
+
+        switch (mMode) {
+            case SCROLL_MODE:
+                long time = AnimationUtils.currentAnimationTimeMillis();
+                // Any scroller can be used for time, since they were started
+                // together in scroll mode. We use X here.
+                final long elapsedTime = time - mScrollerX.mStartTime;
+
+                final int duration = mScrollerX.mDuration;
+                if (elapsedTime < duration) {
+                    float q = (float) (elapsedTime) / duration;
+
+                    if (mInterpolator == null) {
+                        q = Scroller.viscousFluid(q);
+                    } else {
+                        q = mInterpolator.getInterpolation(q);
+                    }
+
+                    mScrollerX.updateScroll(q);
+                    mScrollerY.updateScroll(q);
+                } else {
+                    abortAnimation();
+                }
+                break;
+
+            case FLING_MODE:
+                if (!mScrollerX.mFinished) {
+                    if (!mScrollerX.update()) {
+                        if (!mScrollerX.continueWhenFinished()) {
+                            mScrollerX.finish();
+                        }
+                    }
+                }
+
+                if (!mScrollerY.mFinished) {
+                    if (!mScrollerY.update()) {
+                        if (!mScrollerY.continueWhenFinished()) {
+                            mScrollerY.finish();
+                        }
+                    }
+                }
+
+                break;
+        }
+
+        return true;
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * The scroll will use the default value of 250 milliseconds for the
+     * duration.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy) {
+        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     * @param duration Duration of the scroll in milliseconds.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        mMode = SCROLL_MODE;
+        mScrollerX.startScroll(startX, dx, duration);
+        mScrollerY.startScroll(startY, dy, duration);
+    }
+
+    /**
+     * Call this when you want to 'spring back' into a valid coordinate range.
+     *
+     * @param startX Starting X coordinate
+     * @param startY Starting Y coordinate
+     * @param minX Minimum valid X value
+     * @param maxX Maximum valid X value
+     * @param minY Minimum valid Y value
+     * @param maxY Minimum valid Y value
+     * @return true if a springback was initiated, false if startX and startY were
+     *          already within the valid range.
+     */
+    public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
+        mMode = FLING_MODE;
+
+        // Make sure both methods are called.
+        final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
+        final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
+        return spingbackX || spingbackY;
+    }
+
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance traveled will
+     * depend on the initial velocity of the fling.
+     *
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *            second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *            second
+     * @param minX Minimum X value. The scroller will not scroll past this point
+     *            unless overX > 0. If overfling is allowed, it will use minX as
+     *            a springback boundary.
+     * @param maxX Maximum X value. The scroller will not scroll past this point
+     *            unless overX > 0. If overfling is allowed, it will use maxX as
+     *            a springback boundary.
+     * @param minY Minimum Y value. The scroller will not scroll past this point
+     *            unless overY > 0. If overfling is allowed, it will use minY as
+     *            a springback boundary.
+     * @param maxY Maximum Y value. The scroller will not scroll past this point
+     *            unless overY > 0. If overfling is allowed, it will use maxY as
+     *            a springback boundary.
+     * @param overX Overfling range. If > 0, horizontal overfling in either
+     *            direction will be possible.
+     * @param overY Overfling range. If > 0, vertical overfling in either
+     *            direction will be possible.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY, int overX, int overY) {
+        // Continue a scroll or fling in progress
+        if (mFlywheel && !isFinished()) {
+            float oldVelocityX = mScrollerX.mCurrVelocity;
+            float oldVelocityY = mScrollerY.mCurrVelocity;
+            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
+                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+                velocityX += oldVelocityX;
+                velocityY += oldVelocityY;
+            }
+        }
+
+        mMode = FLING_MODE;
+        mScrollerX.fling(startX, velocityX, minX, maxX, overX);
+        mScrollerY.fling(startY, velocityY, minY, maxY, overY);
+    }
+
+    /**
+     * Notify the scroller that we've reached a horizontal boundary.
+     * Normally the information to handle this will already be known
+     * when the animation is started, such as in a call to one of the
+     * fling functions. However there are cases where this cannot be known
+     * in advance. This function will transition the current motion and
+     * animate from startX to finalX as appropriate.
+     *
+     * @param startX Starting/current X position
+     * @param finalX Desired final X position
+     * @param overX Magnitude of overscroll allowed. This should be the maximum
+     *              desired distance from finalX. Absolute value - must be positive.
+     */
+    public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
+        mScrollerX.notifyEdgeReached(startX, finalX, overX);
+    }
+
+    /**
+     * Notify the scroller that we've reached a vertical boundary.
+     * Normally the information to handle this will already be known
+     * when the animation is started, such as in a call to one of the
+     * fling functions. However there are cases where this cannot be known
+     * in advance. This function will animate a parabolic motion from
+     * startY to finalY.
+     *
+     * @param startY Starting/current Y position
+     * @param finalY Desired final Y position
+     * @param overY Magnitude of overscroll allowed. This should be the maximum
+     *              desired distance from finalY. Absolute value - must be positive.
+     */
+    public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
+        mScrollerY.notifyEdgeReached(startY, finalY, overY);
+    }
+
+    /**
+     * Returns whether the current Scroller is currently returning to a valid position.
+     * Valid bounds were provided by the
+     * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
+     *
+     * One should check this value before calling
+     * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
+     * to restore a valid position will then be stopped. The caller has to take into account
+     * the fact that the started scroll will start from an overscrolled position.
+     *
+     * @return true when the current position is overscrolled and in the process of
+     *         interpolating back to a valid value.
+     */
+    public boolean isOverScrolled() {
+        return ((!mScrollerX.mFinished &&
+                mScrollerX.mState != SplineOverScroller.SPLINE) ||
+                (!mScrollerY.mFinished &&
+                        mScrollerY.mState != SplineOverScroller.SPLINE));
+    }
+
+    /**
+     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+     * aborting the animating causes the scroller to move to the final x and y
+     * positions.
+     *
+     * @see #forceFinished(boolean)
+     */
+    public void abortAnimation() {
+        mScrollerX.finish();
+        mScrollerY.finish();
+    }
+
+    /**
+     * Returns the time elapsed since the beginning of the scrolling.
+     *
+     * @return The elapsed time in milliseconds.
+     *
+     * @hide
+     */
+    public int timePassed() {
+        final long time = AnimationUtils.currentAnimationTimeMillis();
+        final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
+        return (int) (time - startTime);
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isScrollingInDirection(float xvel, float yvel) {
+        final int dx = mScrollerX.mFinal - mScrollerX.mStart;
+        final int dy = mScrollerY.mFinal - mScrollerY.mStart;
+        return !isFinished() && Math.signum(xvel) == Math.signum(dx) &&
+                Math.signum(yvel) == Math.signum(dy);
+    }
+
+    static class SplineOverScroller {
+        // Initial position
+        private int mStart;
+
+        // Current position
+        private int mCurrentPosition;
+
+        // Final position
+        private int mFinal;
+
+        // Initial velocity
+        private int mVelocity;
+
+        // Current velocity
+        private float mCurrVelocity;
+
+        // Constant current deceleration
+        private float mDeceleration;
+
+        // Animation starting time, in system milliseconds
+        private long mStartTime;
+
+        // Animation duration, in milliseconds
+        private int mDuration;
+
+        // Duration to complete spline component of animation
+        private int mSplineDuration;
+
+        // Distance to travel along spline animation
+        private int mSplineDistance;
+
+        // Whether the animation is currently in progress
+        private boolean mFinished;
+
+        // The allowed overshot distance before boundary is reached.
+        private int mOver;
+
+        // Fling friction
+        private float mFlingFriction = ViewConfiguration.getScrollFriction();
+
+        // Current state of the animation.
+        private int mState = SPLINE;
+
+        // Constant gravity value, used in the deceleration phase.
+        private static final float GRAVITY = 2000.0f;
+
+        // A device specific coefficient adjusted to physical values.
+        private static float PHYSICAL_COEF;
+
+        private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
+        private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
+        private static final float START_TENSION = 0.5f;
+        private static final float END_TENSION = 1.0f;
+        private static final float P1 = START_TENSION * INFLEXION;
+        private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
+
+        private static final int NB_SAMPLES = 100;
+        private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
+        private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
+
+        private static final int SPLINE = 0;
+        private static final int CUBIC = 1;
+        private static final int BALLISTIC = 2;
+
+        static {
+            float x_min = 0.0f;
+            float y_min = 0.0f;
+            for (int i = 0; i < NB_SAMPLES; i++) {
+                final float alpha = (float) i / NB_SAMPLES;
+
+                float x_max = 1.0f;
+                float x, tx, coef;
+                while (true) {
+                    x = x_min + (x_max - x_min) / 2.0f;
+                    coef = 3.0f * x * (1.0f - x);
+                    tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
+                    if (Math.abs(tx - alpha) < 1E-5) break;
+                    if (tx > alpha) x_max = x;
+                    else x_min = x;
+                }
+                SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
+
+                float y_max = 1.0f;
+                float y, dy;
+                while (true) {
+                    y = y_min + (y_max - y_min) / 2.0f;
+                    coef = 3.0f * y * (1.0f - y);
+                    dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
+                    if (Math.abs(dy - alpha) < 1E-5) break;
+                    if (dy > alpha) y_max = y;
+                    else y_min = y;
+                }
+                SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
+            }
+            SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
+        }
+
+        static void initFromContext(Context context) {
+            final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+            PHYSICAL_COEF = SensorManager.GRAVITY_EARTH // g (m/s^2)
+                    * 39.37f // inch/meter
+                    * ppi
+                    * 0.84f; // look and feel tuning
+        }
+
+        void setFriction(float friction) {
+            mFlingFriction = friction;
+        }
+
+        SplineOverScroller() {
+            mFinished = true;
+        }
+
+        void updateScroll(float q) {
+            mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
+        }
+
+        /*
+         * Get a signed deceleration that will reduce the velocity.
+         */
+        static private float getDeceleration(int velocity) {
+            return velocity > 0 ? -GRAVITY : GRAVITY;
+        }
+
+        /*
+         * Modifies mDuration to the duration it takes to get from start to newFinal using the
+         * spline interpolation. The previous duration was needed to get to oldFinal.
+         */
+        private void adjustDuration(int start, int oldFinal, int newFinal) {
+            final int oldDistance = oldFinal - start;
+            final int newDistance = newFinal - start;
+            final float x = Math.abs((float) newDistance / oldDistance);
+            final int index = (int) (NB_SAMPLES * x);
+            if (index < NB_SAMPLES) {
+                final float x_inf = (float) index / NB_SAMPLES;
+                final float x_sup = (float) (index + 1) / NB_SAMPLES;
+                final float t_inf = SPLINE_TIME[index];
+                final float t_sup = SPLINE_TIME[index + 1];
+                final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
+                mDuration *= timeCoef;
+            }
+        }
+
+        void startScroll(int start, int distance, int duration) {
+            mFinished = false;
+
+            mStart = start;
+            mFinal = start + distance;
+
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mDuration = duration;
+
+            // Unused
+            mDeceleration = 0.0f;
+            mVelocity = 0;
+        }
+
+        void finish() {
+            mCurrentPosition = mFinal;
+            // Not reset since WebView relies on this value for fast fling.
+            // TODO: restore when WebView uses the fast fling implemented in this class.
+            // mCurrVelocity = 0.0f;
+            mFinished = true;
+        }
+
+        void setFinalPosition(int position) {
+            mFinal = position;
+            mFinished = false;
+        }
+
+        void extendDuration(int extend) {
+            final long time = AnimationUtils.currentAnimationTimeMillis();
+            final int elapsedTime = (int) (time - mStartTime);
+            mDuration = elapsedTime + extend;
+            mFinished = false;
+        }
+
+        boolean springback(int start, int min, int max) {
+            mFinished = true;
+
+            mStart = mFinal = start;
+            mVelocity = 0;
+
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mDuration = 0;
+
+            if (start < min) {
+                startSpringback(start, min, 0);
+            } else if (start > max) {
+                startSpringback(start, max, 0);
+            }
+
+            return !mFinished;
+        }
+
+        private void startSpringback(int start, int end, int velocity) {
+            // mStartTime has been set
+            mFinished = false;
+            mState = CUBIC;
+            mStart = start;
+            mFinal = end;
+            final int delta = start - end;
+            mDeceleration = getDeceleration(delta);
+            // TODO take velocity into account
+            mVelocity = -delta; // only sign is used
+            mOver = Math.abs(delta);
+            mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
+        }
+
+        void fling(int start, int velocity, int min, int max, int over) {
+            mOver = over;
+            mFinished = false;
+            mCurrVelocity = mVelocity = velocity;
+            mDuration = mSplineDuration = 0;
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mCurrentPosition = mStart = start;
+
+            if (start > max || start < min) {
+                startAfterEdge(start, min, max, velocity);
+                return;
+            }
+
+            mState = SPLINE;
+            double totalDistance = 0.0;
+
+            if (velocity != 0) {
+                mDuration = mSplineDuration = getSplineFlingDuration(velocity);
+                totalDistance = getSplineFlingDistance(velocity);
+            }
+
+            mSplineDistance = (int) (totalDistance * Math.signum(velocity));
+            mFinal = start + mSplineDistance;
+
+            // Clamp to a valid final position
+            if (mFinal < min) {
+                adjustDuration(mStart, mFinal, min);
+                mFinal = min;
+            }
+
+            if (mFinal > max) {
+                adjustDuration(mStart, mFinal, max);
+                mFinal = max;
+            }
+        }
+
+        private double getSplineDeceleration(int velocity) {
+            return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * PHYSICAL_COEF));
+        }
+
+        private double getSplineFlingDistance(int velocity) {
+            final double l = getSplineDeceleration(velocity);
+            final double decelMinusOne = DECELERATION_RATE - 1.0;
+            return mFlingFriction * PHYSICAL_COEF * Math.exp(DECELERATION_RATE / decelMinusOne * l);
+        }
+
+        /* Returns the duration, expressed in milliseconds */
+        private int getSplineFlingDuration(int velocity) {
+            final double l = getSplineDeceleration(velocity);
+            final double decelMinusOne = DECELERATION_RATE - 1.0;
+            return (int) (1000.0 * Math.exp(l / decelMinusOne));
+        }
+
+        private void fitOnBounceCurve(int start, int end, int velocity) {
+            // Simulate a bounce that started from edge
+            final float durationToApex = - velocity / mDeceleration;
+            final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration);
+            final float distanceToEdge = Math.abs(end - start);
+            final float totalDuration = (float) Math.sqrt(
+                    2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
+            mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
+            mStart = end;
+            mVelocity = (int) (- mDeceleration * totalDuration);
+        }
+
+        private void startBounceAfterEdge(int start, int end, int velocity) {
+            mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
+            fitOnBounceCurve(start, end, velocity);
+            onEdgeReached();
+        }
+
+        private void startAfterEdge(int start, int min, int max, int velocity) {
+            if (start > min && start < max) {
+                Log.e("OverScroller", "startAfterEdge called from a valid position");
+                mFinished = true;
+                return;
+            }
+            final boolean positive = start > max;
+            final int edge = positive ? max : min;
+            final int overDistance = start - edge;
+            boolean keepIncreasing = overDistance * velocity >= 0;
+            if (keepIncreasing) {
+                // Will result in a bounce or a to_boundary depending on velocity.
+                startBounceAfterEdge(start, edge, velocity);
+            } else {
+                final double totalDistance = getSplineFlingDistance(velocity);
+                if (totalDistance > Math.abs(overDistance)) {
+                    fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
+                } else {
+                    startSpringback(start, edge, velocity);
+                }
+            }
+        }
+
+        void notifyEdgeReached(int start, int end, int over) {
+            // mState is used to detect successive notifications
+            if (mState == SPLINE) {
+                mOver = over;
+                mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                // We were in fling/scroll mode before: current velocity is such that distance to
+                // edge is increasing. This ensures that startAfterEdge will not start a new fling.
+                startAfterEdge(start, end, end, (int) mCurrVelocity);
+            }
+        }
+
+        private void onEdgeReached() {
+            // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
+            float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration));
+            final float sign = Math.signum(mVelocity);
+
+            if (distance > mOver) {
+                // Default deceleration is not sufficient to slow us down before boundary
+                 mDeceleration = - sign * mVelocity * mVelocity / (2.0f * mOver);
+                 distance = mOver;
+            }
+
+            mOver = (int) distance;
+            mState = BALLISTIC;
+            mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
+            mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
+        }
+
+        boolean continueWhenFinished() {
+            switch (mState) {
+                case SPLINE:
+                    // Duration from start to null velocity
+                    if (mDuration < mSplineDuration) {
+                        // If the animation was clamped, we reached the edge
+                        mStart = mFinal;
+                        // TODO Better compute speed when edge was reached
+                        mVelocity = (int) mCurrVelocity;
+                        mDeceleration = getDeceleration(mVelocity);
+                        mStartTime += mDuration;
+                        onEdgeReached();
+                    } else {
+                        // Normal stop, no need to continue
+                        return false;
+                    }
+                    break;
+                case BALLISTIC:
+                    mStartTime += mDuration;
+                    startSpringback(mFinal, mStart, 0);
+                    break;
+                case CUBIC:
+                    return false;
+            }
+
+            update();
+            return true;
+        }
+
+        /*
+         * Update the current position and velocity for current time. Returns
+         * true if update has been done and false if animation duration has been
+         * reached.
+         */
+        boolean update() {
+            final long time = AnimationUtils.currentAnimationTimeMillis();
+            final long currentTime = time - mStartTime;
+
+            if (currentTime > mDuration) {
+                return false;
+            }
+
+            double distance = 0.0;
+            switch (mState) {
+                case SPLINE: {
+                    final float t = (float) currentTime / mSplineDuration;
+                    final int index = (int) (NB_SAMPLES * t);
+                    float distanceCoef = 1.f;
+                    float velocityCoef = 0.f;
+                    if (index < NB_SAMPLES) {
+                        final float t_inf = (float) index / NB_SAMPLES;
+                        final float t_sup = (float) (index + 1) / NB_SAMPLES;
+                        final float d_inf = SPLINE_POSITION[index];
+                        final float d_sup = SPLINE_POSITION[index + 1];
+                        velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
+                        distanceCoef = d_inf + (t - t_inf) * velocityCoef;
+                    }
+
+                    distance = distanceCoef * mSplineDistance;
+                    mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
+                    break;
+                }
+
+                case BALLISTIC: {
+                    final float t = currentTime / 1000.0f;
+                    mCurrVelocity = mVelocity + mDeceleration * t;
+                    distance = mVelocity * t + mDeceleration * t * t / 2.0f;
+                    break;
+                }
+
+                case CUBIC: {
+                    final float t = (float) (currentTime) / mDuration;
+                    final float t2 = t * t;
+                    final float sign = Math.signum(mVelocity);
+                    distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
+                    mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
+                    break;
+                }
+            }
+
+            mCurrentPosition = mStart + (int) Math.round(distance);
+
+            return true;
+        }
+    }
+}
diff --git a/gallerycommon/src/com/android/gallery3d/common/Scroller.java b/gallerycommon/src/com/android/gallery3d/common/Scroller.java
new file mode 100644
index 0000000..160f3ca
--- /dev/null
+++ b/gallerycommon/src/com/android/gallery3d/common/Scroller.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2006 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.gallery3d.common;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.os.Build;
+import android.util.FloatMath;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+
+/**
+ * This class encapsulates scrolling.  The duration of the scroll
+ * can be passed in the constructor and specifies the maximum time that
+ * the scrolling animation should take.  Past this time, the scrolling is
+ * automatically moved to its final stage and computeScrollOffset()
+ * will always return false to indicate that scrolling is over.
+ */
+public class Scroller  {
+    private int mMode;
+
+    private int mStartX;
+    private int mStartY;
+    private int mFinalX;
+    private int mFinalY;
+
+    private int mMinX;
+    private int mMaxX;
+    private int mMinY;
+    private int mMaxY;
+
+    private int mCurrX;
+    private int mCurrY;
+    private long mStartTime;
+    private int mDuration;
+    private float mDurationReciprocal;
+    private float mDeltaX;
+    private float mDeltaY;
+    private boolean mFinished;
+    private Interpolator mInterpolator;
+    private boolean mFlywheel;
+
+    private float mVelocity;
+
+    private static final int DEFAULT_DURATION = 250;
+    private static final int SCROLL_MODE = 0;
+    private static final int FLING_MODE = 1;
+
+    private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9));
+    private static float ALPHA = 800; // pixels / seconds
+    private static float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance)
+    private static float END_TENSION = 1.0f - START_TENSION;
+    private static final int NB_SAMPLES = 100;
+    private static final float[] SPLINE = new float[NB_SAMPLES + 1];
+
+    private float mDeceleration;
+    private final float mPpi;
+
+    static {
+        float x_min = 0.0f;
+        for (int i = 0; i <= NB_SAMPLES; i++) {
+            final float t = (float) i / NB_SAMPLES;
+            float x_max = 1.0f;
+            float x, tx, coef;
+            while (true) {
+                x = x_min + (x_max - x_min) / 2.0f;
+                coef = 3.0f * x * (1.0f - x);
+                tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x;
+                if (Math.abs(tx - t) < 1E-5) break;
+                if (tx > t) x_max = x;
+                else x_min = x;
+            }
+            final float d = coef + x * x * x;
+            SPLINE[i] = d;
+        }
+        SPLINE[NB_SAMPLES] = 1.0f;
+
+        // This controls the viscous fluid effect (how much of it)
+        sViscousFluidScale = 8.0f;
+        // must be set to 1.0 (used in viscousFluid())
+        sViscousFluidNormalize = 1.0f;
+        sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
+    }
+
+    private static float sViscousFluidScale;
+    private static float sViscousFluidNormalize;
+
+    /**
+     * Create a Scroller with the default duration and interpolator.
+     */
+    public Scroller(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
+     * be in effect for apps targeting Honeycomb or newer.
+     */
+    public Scroller(Context context, Interpolator interpolator) {
+        this(context, interpolator,
+                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
+    }
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. Specify whether or
+     * not to support progressive "flywheel" behavior in flinging.
+     */
+    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
+        mFinished = true;
+        mInterpolator = interpolator;
+        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
+        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
+        mFlywheel = flywheel;
+    }
+
+    /**
+     * The amount of friction applied to flings. The default value
+     * is {@link ViewConfiguration#getScrollFriction}.
+     *
+     * @param friction A scalar dimension-less value representing the coefficient of
+     *         friction.
+     */
+    public final void setFriction(float friction) {
+        mDeceleration = computeDeceleration(friction);
+    }
+
+    private float computeDeceleration(float friction) {
+        return SensorManager.GRAVITY_EARTH   // g (m/s^2)
+                      * 39.37f               // inch/meter
+                      * mPpi                 // pixels per inch
+                      * friction;
+    }
+
+    /**
+     *
+     * Returns whether the scroller has finished scrolling.
+     *
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mFinished;
+    }
+
+    /**
+     * Force the finished field to a particular value.
+     *
+     * @param finished The new finished value.
+     */
+    public final void forceFinished(boolean finished) {
+        mFinished = finished;
+    }
+
+    /**
+     * Returns how long the scroll event will take, in milliseconds.
+     *
+     * @return The duration of the scroll in milliseconds.
+     */
+    public final int getDuration() {
+        return mDuration;
+    }
+
+    /**
+     * Returns the current X offset in the scroll.
+     *
+     * @return The new X offset as an absolute distance from the origin.
+     */
+    public final int getCurrX() {
+        return mCurrX;
+    }
+
+    /**
+     * Returns the current Y offset in the scroll.
+     *
+     * @return The new Y offset as an absolute distance from the origin.
+     */
+    public final int getCurrY() {
+        return mCurrY;
+    }
+
+    /**
+     * Returns the current velocity.
+     *
+     * @return The original velocity less the deceleration. Result may be
+     * negative.
+     */
+    public float getCurrVelocity() {
+        return mVelocity - mDeceleration * timePassed() / 2000.0f;
+    }
+
+    /**
+     * Returns the start X offset in the scroll.
+     *
+     * @return The start X offset as an absolute distance from the origin.
+     */
+    public final int getStartX() {
+        return mStartX;
+    }
+
+    /**
+     * Returns the start Y offset in the scroll.
+     *
+     * @return The start Y offset as an absolute distance from the origin.
+     */
+    public final int getStartY() {
+        return mStartY;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final X offset as an absolute distance from the origin.
+     */
+    public final int getFinalX() {
+        return mFinalX;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final Y offset as an absolute distance from the origin.
+     */
+    public final int getFinalY() {
+        return mFinalY;
+    }
+
+    /**
+     * Call this when you want to know the new location.  If it returns true,
+     * the animation is not yet finished.  loc will be altered to provide the
+     * new location.
+     */
+    public boolean computeScrollOffset() {
+        if (mFinished) {
+            return false;
+        }
+
+        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+
+        if (timePassed < mDuration) {
+            switch (mMode) {
+            case SCROLL_MODE:
+                float x = timePassed * mDurationReciprocal;
+
+                if (mInterpolator == null)
+                    x = viscousFluid(x);
+                else
+                    x = mInterpolator.getInterpolation(x);
+
+                mCurrX = mStartX + Math.round(x * mDeltaX);
+                mCurrY = mStartY + Math.round(x * mDeltaY);
+                break;
+            case FLING_MODE:
+                final float t = (float) timePassed / mDuration;
+                final int index = (int) (NB_SAMPLES * t);
+                final float t_inf = (float) index / NB_SAMPLES;
+                final float t_sup = (float) (index + 1) / NB_SAMPLES;
+                final float d_inf = SPLINE[index];
+                final float d_sup = SPLINE[index + 1];
+                final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);
+
+                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
+                // Pin to mMinX <= mCurrX <= mMaxX
+                mCurrX = Math.min(mCurrX, mMaxX);
+                mCurrX = Math.max(mCurrX, mMinX);
+
+                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
+                // Pin to mMinY <= mCurrY <= mMaxY
+                mCurrY = Math.min(mCurrY, mMaxY);
+                mCurrY = Math.max(mCurrY, mMinY);
+
+                if (mCurrX == mFinalX && mCurrY == mFinalY) {
+                    mFinished = true;
+                }
+
+                break;
+            }
+        }
+        else {
+            mCurrX = mFinalX;
+            mCurrY = mFinalY;
+            mFinished = true;
+        }
+        return true;
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * The scroll will use the default value of 250 milliseconds for the
+     * duration.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy) {
+        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     * @param duration Duration of the scroll in milliseconds.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        mMode = SCROLL_MODE;
+        mFinished = false;
+        mDuration = duration;
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartX = startX;
+        mStartY = startY;
+        mFinalX = startX + dx;
+        mFinalY = startY + dy;
+        mDeltaX = dx;
+        mDeltaY = dy;
+        mDurationReciprocal = 1.0f / (float) mDuration;
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance travelled will
+     * depend on the initial velocity of the fling.
+     *
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *        second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *        second
+     * @param minX Minimum X value. The scroller will not scroll past this
+     *        point.
+     * @param maxX Maximum X value. The scroller will not scroll past this
+     *        point.
+     * @param minY Minimum Y value. The scroller will not scroll past this
+     *        point.
+     * @param maxY Maximum Y value. The scroller will not scroll past this
+     *        point.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        // Continue a scroll or fling in progress
+        if (mFlywheel && !mFinished) {
+            float oldVel = getCurrVelocity();
+
+            float dx = (float) (mFinalX - mStartX);
+            float dy = (float) (mFinalY - mStartY);
+            float hyp = FloatMath.sqrt(dx * dx + dy * dy);
+
+            float ndx = dx / hyp;
+            float ndy = dy / hyp;
+
+            float oldVelocityX = ndx * oldVel;
+            float oldVelocityY = ndy * oldVel;
+            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
+                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+                velocityX += oldVelocityX;
+                velocityY += oldVelocityY;
+            }
+        }
+
+        mMode = FLING_MODE;
+        mFinished = false;
+
+        float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
+
+        mVelocity = velocity;
+        final double l = Math.log(START_TENSION * velocity / ALPHA);
+        mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0)));
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartX = startX;
+        mStartY = startY;
+
+        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
+        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
+
+        int totalDistance =
+                (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l));
+
+        mMinX = minX;
+        mMaxX = maxX;
+        mMinY = minY;
+        mMaxY = maxY;
+
+        mFinalX = startX + Math.round(totalDistance * coeffX);
+        // Pin to mMinX <= mFinalX <= mMaxX
+        mFinalX = Math.min(mFinalX, mMaxX);
+        mFinalX = Math.max(mFinalX, mMinX);
+
+        mFinalY = startY + Math.round(totalDistance * coeffY);
+        // Pin to mMinY <= mFinalY <= mMaxY
+        mFinalY = Math.min(mFinalY, mMaxY);
+        mFinalY = Math.max(mFinalY, mMinY);
+    }
+
+    static float viscousFluid(float x)
+    {
+        x *= sViscousFluidScale;
+        if (x < 1.0f) {
+            x -= (1.0f - (float)Math.exp(-x));
+        } else {
+            float start = 0.36787944117f;   // 1/e == exp(-1)
+            x = 1.0f - (float)Math.exp(1.0f - x);
+            x = start + x * (1.0f - start);
+        }
+        x *= sViscousFluidNormalize;
+        return x;
+    }
+
+    /**
+     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+     * aborting the animating cause the scroller to move to the final x and y
+     * position
+     *
+     * @see #forceFinished(boolean)
+     */
+    public void abortAnimation() {
+        mCurrX = mFinalX;
+        mCurrY = mFinalY;
+        mFinished = true;
+    }
+
+    /**
+     * Extend the scroll animation. This allows a running animation to scroll
+     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+     *
+     * @param extend Additional time to scroll in milliseconds.
+     * @see #setFinalX(int)
+     * @see #setFinalY(int)
+     */
+    public void extendDuration(int extend) {
+        int passed = timePassed();
+        mDuration = passed + extend;
+        mDurationReciprocal = 1.0f / mDuration;
+        mFinished = false;
+    }
+
+    /**
+     * Returns the time elapsed since the beginning of the scrolling.
+     *
+     * @return The elapsed time in milliseconds.
+     */
+    public int timePassed() {
+        return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+    }
+
+    /**
+     * Sets the final position (X) for this scroller.
+     *
+     * @param newX The new X offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalY(int)
+     */
+    public void setFinalX(int newX) {
+        mFinalX = newX;
+        mDeltaX = mFinalX - mStartX;
+        mFinished = false;
+    }
+
+    /**
+     * Sets the final position (Y) for this scroller.
+     *
+     * @param newY The new Y offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalX(int)
+     */
+    public void setFinalY(int newY) {
+        mFinalY = newY;
+        mDeltaY = mFinalY - mStartY;
+        mFinished = false;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isScrollingInDirection(float xvel, float yvel) {
+        return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
+                Math.signum(yvel) == Math.signum(mFinalY - mStartY);
+    }
+}
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
index 0987ac3..6b0b106 100644
--- a/src/com/android/gallery3d/app/AbstractGalleryActivity.java
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.app;
 
+import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.BroadcastReceiver;
@@ -27,11 +28,14 @@
 import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.view.Menu;
 import android.view.MenuItem;
 import android.view.Window;
 import android.view.WindowManager;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.BitmapPool;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.ui.GLRoot;
@@ -84,18 +88,42 @@
         toggleStatusBarByOrientation();
     }
 
+    private Menu mOptionsMenu;
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        mOptionsMenu = menu;
+        super.onCreateOptionsMenu(menu);
+        return getStateManager().createOptionsMenu(menu);
+    }
+
+    @Override
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    public void invalidateOptionsMenu() {
+        if (ApiHelper.HAS_ACTIVITY_INVALIDATE_OPTIONS_MENU) {
+            super.invalidateOptionsMenu();
+        } else if (mOptionsMenu != null) {
+            mOptionsMenu.clear();
+            getStateManager().createOptionsMenu(mOptionsMenu);
+        }
+    }
+
+    @Override
     public Context getAndroidContext() {
         return this;
     }
 
+    @Override
     public DataManager getDataManager() {
         return ((GalleryApp) getApplication()).getDataManager();
     }
 
+    @Override
     public ThreadPool getThreadPool() {
         return ((GalleryApp) getApplication()).getThreadPool();
     }
 
+    @Override
     public synchronized StateManager getStateManager() {
         if (mStateManager == null) {
             mStateManager = new StateManager(this);
@@ -103,10 +131,12 @@
         return mStateManager;
     }
 
+    @Override
     public GLRoot getGLRoot() {
         return mGLRootView;
     }
 
+    @Override
     public OrientationManager getOrientationManager() {
         return mOrientationManager;
     }
@@ -141,17 +171,27 @@
                     dialog.cancel();
                 }
             };
-            mAlertDialog = new AlertDialog.Builder(this)
-                    .setIconAttribute(android.R.attr.alertDialogIcon)
+            AlertDialog.Builder builder = new AlertDialog.Builder(this)
                     .setTitle(R.string.no_external_storage_title)
                     .setMessage(R.string.no_external_storage)
                     .setNegativeButton(android.R.string.cancel, onClick)
-                    .setOnCancelListener(onCancel)
-                    .show();
+                    .setOnCancelListener(onCancel);
+            if (ApiHelper.HAS_SET_ICON_ATTRIBUTE) {
+                setAlertDialogIconAttribute(builder);
+            } else {
+                builder.setIcon(android.R.drawable.ic_dialog_alert);
+            }
+            mAlertDialog = builder.show();
             registerReceiver(mMountReceiver, mMountFilter);
         }
     }
 
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+    private static void setAlertDialogIconAttribute(
+            AlertDialog.Builder builder) {
+        builder.setIconAttribute(android.R.attr.alertDialogIcon);
+    }
+
     @Override
     protected void onStop() {
         super.onStop();
@@ -188,11 +228,16 @@
         } finally {
             mGLRootView.unlockRenderThread();
         }
-        MediaItem.getMicroThumbPool().clear();
-        MediaItem.getThumbPool().clear();
+        clearBitmapPool(MediaItem.getMicroThumbPool());
+        clearBitmapPool(MediaItem.getThumbPool());
+
         MediaItem.getBytesBufferPool().clear();
     }
 
+    private static void clearBitmapPool(BitmapPool pool) {
+        if (pool != null) pool.clear();
+    }
+
     @Override
     protected void onDestroy() {
         super.onDestroy();
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
index 867cda3..c870855 100644
--- a/src/com/android/gallery3d/app/ActivityState.java
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -27,7 +27,6 @@
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
 
@@ -135,7 +134,7 @@
 
     // should only be called by StateManager
     void resume() {
-        Activity activity = (Activity) mActivity;
+        AbstractGalleryActivity activity = (AbstractGalleryActivity) mActivity;
         ActionBar actionBar = activity.getActionBar();
         if (actionBar != null) {
             if ((mFlags & FLAG_HIDE_ACTION_BAR) != 0) {
diff --git a/src/com/android/gallery3d/app/CropImage.java b/src/com/android/gallery3d/app/CropImage.java
index f264697..a587624 100644
--- a/src/com/android/gallery3d/app/CropImage.java
+++ b/src/com/android/gallery3d/app/CropImage.java
@@ -33,6 +33,7 @@
 import android.graphics.RectF;
 import android.media.ExifInterface;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
@@ -48,6 +49,7 @@
 import com.android.gallery3d.R;
 import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.ExifTags;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.LocalImage;
@@ -369,7 +371,7 @@
         }
     }
 
-    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     private static void setImageSize(ContentValues values, int width, int height) {
         // The two fields are available since ICS but got published in JB
         if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
@@ -988,11 +990,11 @@
             }
 
             // Handle some special values here
-            String value = oldExif.getAttribute(ExifInterface.TAG_APERTURE);
+            String value = oldExif.getAttribute(ExifTags.TAG_APERTURE);
             if (value != null) {
                 try {
                     float aperture = Float.parseFloat(value);
-                    newExif.setAttribute(ExifInterface.TAG_APERTURE,
+                    newExif.setAttribute(ExifTags.TAG_APERTURE,
                             String.valueOf((int) (aperture * 10 + 0.5f)) + "/10");
                 } catch (NumberFormatException e) {
                     Log.w(TAG, "cannot parse aperture: " + value);
@@ -1001,22 +1003,22 @@
 
             // TODO: The code is broken, need to fix the JHEAD lib
             /*
-            value = oldExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
+            value = oldExif.getAttribute(ExifTags.TAG_EXPOSURE_TIME);
             if (value != null) {
                 try {
                     double exposure = Double.parseDouble(value);
                     testToRational("test exposure", exposure);
-                    newExif.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, value);
+                    newExif.setAttribute(ExifTags.TAG_EXPOSURE_TIME, value);
                 } catch (NumberFormatException e) {
                     Log.w(TAG, "cannot parse exposure time: " + value);
                 }
             }
 
-            value = oldExif.getAttribute(ExifInterface.TAG_ISO);
+            value = oldExif.getAttribute(ExifTags.TAG_ISO);
             if (value != null) {
                 try {
                     int iso = Integer.parseInt(value);
-                    newExif.setAttribute(ExifInterface.TAG_ISO, String.valueOf(iso) + "/1");
+                    newExif.setAttribute(ExifTags.TAG_ISO, String.valueOf(iso) + "/1");
                 } catch (NumberFormatException e) {
                     Log.w(TAG, "cannot parse exposure time: " + value);
                 }
diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java
index 468d202..0d5770c 100644
--- a/src/com/android/gallery3d/app/Gallery.java
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -26,7 +26,6 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.OpenableColumns;
-import android.view.Menu;
 import android.view.Window;
 import android.widget.Toast;
 
@@ -37,7 +36,6 @@
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.Path;
 import com.android.gallery3d.picasasource.PicasaSource;
-import com.android.gallery3d.ui.GLRoot;
 import com.android.gallery3d.util.GalleryUtils;
 
 public final class Gallery extends AbstractGalleryActivity implements OnCancelListener {
@@ -232,12 +230,6 @@
     }
 
     @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        super.onCreateOptionsMenu(menu);
-        return getStateManager().createOptionsMenu(menu);
-    }
-
-    @Override
     protected void onResume() {
         Utils.assertTrue(getStateManager().getStateCount() > 0);
         super.onResume();
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
index 98d7a64..f2dc9ad 100644
--- a/src/com/android/gallery3d/app/MoviePlayer.java
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -29,6 +29,7 @@
 import android.media.AudioManager;
 import android.media.MediaPlayer;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.view.KeyEvent;
@@ -57,6 +58,10 @@
     private static final String KEY_VIDEO_POSITION = "video-position";
     private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
 
+    // These are constants in KeyEvent, appearing on API level 11.
+    private static final int KEYCODE_MEDIA_PLAY = 126;
+    private static final int KEYCODE_MEDIA_PAUSE = 127;
+
     // Copied from MediaPlaybackService in the Music Player app.
     private static final String SERVICECMD = "com.android.music.musicservicecommand";
     private static final String CMDNAME = "command";
@@ -178,7 +183,7 @@
         }
     }
 
-    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     private void setOnSystemUiVisibilityChangeListener() {
         if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION) return;
 
@@ -212,7 +217,7 @@
         });
     }
 
-    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     private void showSystemUi(boolean visible) {
         if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) return;
 
@@ -220,7 +225,7 @@
                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
         if (!visible) {
-            flag |= View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN
+            flag |= View.STATUS_BAR_HIDDEN | View.SYSTEM_UI_FLAG_FULLSCREEN
                     | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
         }
         mVideoView.setSystemUiVisibility(flag);
@@ -410,12 +415,12 @@
                     playVideo();
                 }
                 return true;
-            case KeyEvent.KEYCODE_MEDIA_PAUSE:
+            case KEYCODE_MEDIA_PAUSE:
                 if (mVideoView.isPlaying()) {
                     pauseVideo();
                 }
                 return true;
-            case KeyEvent.KEYCODE_MEDIA_PLAY:
+            case KEYCODE_MEDIA_PLAY:
                 if (!mVideoView.isPlaying()) {
                     playVideo();
                 }
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index 22b0163..3b6a770 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -321,10 +321,9 @@
         }
     }
 
-    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
     private void setNfcBeamPushUris(Uri[] uris) {
-        if (mNfcAdapter != null &&
-                Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.JELLY_BEAN) {
+        if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
             mNfcAdapter.setBeamPushUris(uris, (Activity)mActivity);
         }
     }
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
index 1ece66c..996d3f0 100644
--- a/src/com/android/gallery3d/app/Wallpaper.java
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -63,7 +63,7 @@
     }
 
     @SuppressWarnings("deprecation")
-    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB_MR2)
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
     private Point getDefaultDisplaySize(Point size) {
         Display d = getWindowManager().getDefaultDisplay();
         if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) {
diff --git a/src/com/android/gallery3d/data/BitmapPool.java b/src/com/android/gallery3d/data/BitmapPool.java
index 0fbd84e..1151dde 100644
--- a/src/com/android/gallery3d/data/BitmapPool.java
+++ b/src/com/android/gallery3d/data/BitmapPool.java
@@ -17,14 +17,9 @@
 package com.android.gallery3d.data;
 
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.BitmapFactory.Options;
 
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.ui.Log;
-import com.android.gallery3d.util.ThreadPool.JobContext;
 
-import java.io.FileDescriptor;
 import java.util.ArrayList;
 
 public class BitmapPool {
@@ -94,67 +89,7 @@
         mPool.clear();
     }
 
-    private Bitmap findCachedBitmap(JobContext jc,
-            byte[] data, int offset, int length, Options options) {
-        if (mOneSize) return getBitmap();
-        DecodeUtils.decodeBounds(jc, data, offset, length, options);
-        return getBitmap(options.outWidth, options.outHeight);
-    }
-
-    private Bitmap findCachedBitmap(JobContext jc,
-            FileDescriptor fileDescriptor, Options options) {
-        if (mOneSize) return getBitmap();
-        DecodeUtils.decodeBounds(jc, fileDescriptor, options);
-        return getBitmap(options.outWidth, options.outHeight);
-    }
-
-    public Bitmap decode(JobContext jc,
-            byte[] data, int offset, int length, BitmapFactory.Options options) {
-        if (options == null) options = new BitmapFactory.Options();
-        if (options.inSampleSize < 1) options.inSampleSize = 1;
-        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
-        options.inBitmap = (options.inSampleSize == 1)
-                ? findCachedBitmap(jc, data, offset, length, options) : null;
-        try {
-            Bitmap bitmap = DecodeUtils.decode(jc, data, offset, length, options);
-            if (options.inBitmap != null && options.inBitmap != bitmap) {
-                recycle(options.inBitmap);
-                options.inBitmap = null;
-            }
-            return bitmap;
-        } catch (IllegalArgumentException e) {
-            if (options.inBitmap == null) throw e;
-
-            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
-            recycle(options.inBitmap);
-            options.inBitmap = null;
-            return DecodeUtils.decode(jc, data, offset, length, options);
-        }
-    }
-
-    // This is the same as the method above except the source data comes
-    // from a file descriptor instead of a byte array.
-    public Bitmap decode(JobContext jc,
-            FileDescriptor fileDescriptor, Options options) {
-        if (options == null) options = new BitmapFactory.Options();
-        if (options.inSampleSize < 1) options.inSampleSize = 1;
-        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
-        options.inBitmap = (options.inSampleSize == 1)
-                ? findCachedBitmap(jc, fileDescriptor, options) : null;
-        try {
-            Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options);
-            if (options.inBitmap != null&& options.inBitmap != bitmap) {
-                recycle(options.inBitmap);
-                options.inBitmap = null;
-            }
-            return bitmap;
-        } catch (IllegalArgumentException e) {
-            if (options.inBitmap == null) throw e;
-
-            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
-            recycle(options.inBitmap);
-            options.inBitmap = null;
-            return DecodeUtils.decode(jc, fileDescriptor, options);
-        }
+    public boolean isOneSize() {
+        return mOneSize;
     }
 }
diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
index 8551327..9ffdc0b 100644
--- a/src/com/android/gallery3d/data/DataManager.java
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -23,6 +23,7 @@
 import android.support.v4.content.LocalBroadcastManager;
 
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.MediaSet.ItemConsumer;
 import com.android.gallery3d.data.MediaSource.PathId;
@@ -115,7 +116,9 @@
         // the order matters, the UriSource must come last
         addSource(new LocalSource(mApplication));
         addSource(new PicasaSource(mApplication));
-        addSource(new MtpSource(mApplication));
+        if (ApiHelper.HAS_MTP) {
+            addSource(new MtpSource(mApplication));
+        }
         addSource(new ComboSource(mApplication));
         addSource(new ClusterSource(mApplication));
         addSource(new FilterSource(mApplication));
diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java
index e51dc3f..773af74 100644
--- a/src/com/android/gallery3d/data/DecodeUtils.java
+++ b/src/com/android/gallery3d/data/DecodeUtils.java
@@ -16,15 +16,18 @@
 
 package com.android.gallery3d.data;
 
+import android.annotation.TargetApi;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapFactory.Options;
 import android.graphics.BitmapRegionDecoder;
+import android.os.Build;
 import android.util.FloatMath;
 
 import com.android.gallery3d.common.BitmapUtils;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.Log;
 import com.android.gallery3d.util.ThreadPool.CancelListener;
 import com.android.gallery3d.util.ThreadPool.JobContext;
 
@@ -33,7 +36,7 @@
 import java.io.InputStream;
 
 public class DecodeUtils {
-    private static final String TAG = "DecodeService";
+    private static final String TAG = "DecodeUtils";
 
     private static class DecodeCanceller implements CancelListener {
         Options mOptions;
@@ -230,4 +233,78 @@
             return null;
         }
     }
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static Bitmap decode(JobContext jc, byte[] data, int offset,
+            int length, BitmapFactory.Options options, BitmapPool pool) {
+        if (pool == null) {
+            return decode(jc, data, offset, length, options);
+        }
+
+        if (options == null) options = new BitmapFactory.Options();
+        if (options.inSampleSize < 1) options.inSampleSize = 1;
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        options.inBitmap = (options.inSampleSize == 1)
+                ? findCachedBitmap(pool, jc, data, offset, length, options) : null;
+        try {
+            Bitmap bitmap = decode(jc, data, offset, length, options);
+            if (options.inBitmap != null && options.inBitmap != bitmap) {
+                pool.recycle(options.inBitmap);
+                options.inBitmap = null;
+            }
+            return bitmap;
+        } catch (IllegalArgumentException e) {
+            if (options.inBitmap == null) throw e;
+
+            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+            pool.recycle(options.inBitmap);
+            options.inBitmap = null;
+            return decode(jc, data, offset, length, options);
+        }
+    }
+
+    // This is the same as the method above except the source data comes
+    // from a file descriptor instead of a byte array.
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public static Bitmap decode(JobContext jc,
+            FileDescriptor fileDescriptor, Options options, BitmapPool pool) {
+        if (pool == null) {
+            return decode(jc, fileDescriptor, options);
+        }
+
+        if (options == null) options = new BitmapFactory.Options();
+        if (options.inSampleSize < 1) options.inSampleSize = 1;
+        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        options.inBitmap = (options.inSampleSize == 1)
+                ? findCachedBitmap(pool, jc, fileDescriptor, options) : null;
+        try {
+            Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options);
+            if (options.inBitmap != null && options.inBitmap != bitmap) {
+                pool.recycle(options.inBitmap);
+                options.inBitmap = null;
+            }
+            return bitmap;
+        } catch (IllegalArgumentException e) {
+            if (options.inBitmap == null) throw e;
+
+            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+            pool.recycle(options.inBitmap);
+            options.inBitmap = null;
+            return decode(jc, fileDescriptor, options);
+        }
+    }
+
+    private static Bitmap findCachedBitmap(BitmapPool pool, JobContext jc,
+            byte[] data, int offset, int length, Options options) {
+        if (pool.isOneSize()) return pool.getBitmap();
+        decodeBounds(jc, data, offset, length, options);
+        return pool.getBitmap(options.outWidth, options.outHeight);
+    }
+
+    private static Bitmap findCachedBitmap(BitmapPool pool, JobContext jc,
+            FileDescriptor fileDescriptor, Options options) {
+        if (pool.isOneSize()) return pool.getBitmap();
+        decodeBounds(jc, fileDescriptor, options);
+        return pool.getBitmap(options.outWidth, options.outHeight);
+    }
 }
diff --git a/src/com/android/gallery3d/data/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java
index 81660c9..b977b10 100644
--- a/src/com/android/gallery3d/data/ImageCacheRequest.java
+++ b/src/com/android/gallery3d/data/ImageCacheRequest.java
@@ -57,11 +57,13 @@
                 options.inPreferredConfig = Bitmap.Config.ARGB_8888;
                 Bitmap bitmap;
                 if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
-                    bitmap = MediaItem.getMicroThumbPool().decode(jc,
-                            buffer.data, buffer.offset, buffer.length, options);
+                    bitmap = DecodeUtils.decode(jc,
+                            buffer.data, buffer.offset, buffer.length, options,
+                            MediaItem.getMicroThumbPool());
                 } else {
-                    bitmap = MediaItem.getThumbPool().decode(jc,
-                            buffer.data, buffer.offset, buffer.length, options);
+                    bitmap = DecodeUtils.decode(jc,
+                            buffer.data, buffer.offset, buffer.length, options,
+                            MediaItem.getThumbPool());
                 }
                 if (bitmap == null && !jc.isCancelled()) {
                     Log.w(TAG, "decode cached failed " + debugTag);
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index 316e324..c432ab4 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -25,6 +25,7 @@
 import android.graphics.BitmapRegionDecoder;
 import android.media.ExifInterface;
 import android.net.Uri;
+import android.os.Build;
 import android.provider.MediaStore.Images;
 import android.provider.MediaStore.Images.ImageColumns;
 import android.provider.MediaStore.MediaColumns;
@@ -85,7 +86,7 @@
         updateWidthAndHeightProjection();
     }
 
-    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     private static void updateWidthAndHeightProjection() {
         if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
             PROJECTION[INDEX_WIDTH] = MediaColumns.WIDTH;
diff --git a/src/com/android/gallery3d/data/MediaDetails.java b/src/com/android/gallery3d/data/MediaDetails.java
index 9663dd0..c18365d 100644
--- a/src/com/android/gallery3d/data/MediaDetails.java
+++ b/src/com/android/gallery3d/data/MediaDetails.java
@@ -19,6 +19,7 @@
 import android.media.ExifInterface;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.common.ExifTags;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -126,11 +127,11 @@
                     MediaDetails.INDEX_HEIGHT);
             setExifData(details, exif, ExifInterface.TAG_MAKE, MediaDetails.INDEX_MAKE);
             setExifData(details, exif, ExifInterface.TAG_MODEL, MediaDetails.INDEX_MODEL);
-            setExifData(details, exif, ExifInterface.TAG_APERTURE, MediaDetails.INDEX_APERTURE);
-            setExifData(details, exif, ExifInterface.TAG_ISO, MediaDetails.INDEX_ISO);
+            setExifData(details, exif, ExifTags.TAG_APERTURE, MediaDetails.INDEX_APERTURE);
+            setExifData(details, exif, ExifTags.TAG_ISO, MediaDetails.INDEX_ISO);
             setExifData(details, exif, ExifInterface.TAG_WHITE_BALANCE,
                     MediaDetails.INDEX_WHITE_BALANCE);
-            setExifData(details, exif, ExifInterface.TAG_EXPOSURE_TIME,
+            setExifData(details, exif, ExifTags.TAG_EXPOSURE_TIME,
                     MediaDetails.INDEX_EXPOSURE_TIME);
 
             double data = exif.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, 0);
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
index f0f1af4..15bb176 100644
--- a/src/com/android/gallery3d/data/MediaItem.java
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -19,6 +19,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapRegionDecoder;
 
+import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.ui.ScreenNail;
 import com.android.gallery3d.util.ThreadPool.Job;
 
@@ -42,9 +43,16 @@
     private static final int BYTESBUFFE_POOL_SIZE = 4;
     private static final int BYTESBUFFER_SIZE = 200 * 1024;
 
-    private static final BitmapPool sMicroThumbPool = new BitmapPool(
-            MICROTHUMBNAIL_TARGET_SIZE, MICROTHUMBNAIL_TARGET_SIZE, 16);
-    private static final BitmapPool sThumbPool = new BitmapPool(4);
+    private static final BitmapPool sMicroThumbPool =
+            ApiHelper.HAS_REUSING_BITMAP_IN_BITMAP_FACTORY
+            ? new BitmapPool(MICROTHUMBNAIL_TARGET_SIZE, MICROTHUMBNAIL_TARGET_SIZE, 16)
+            : null;
+
+    private static final BitmapPool sThumbPool =
+            ApiHelper.HAS_REUSING_BITMAP_IN_BITMAP_FACTORY
+            ? new BitmapPool(4)
+            : null;
+
     private static final BytesBufferPool sMicroThumbBufferPool =
             new BytesBufferPool(BYTESBUFFE_POOL_SIZE, BYTESBUFFER_SIZE);
 
diff --git a/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
index 9813902..58466bf 100644
--- a/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
+++ b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.gadget;
 
+import android.annotation.TargetApi;
 import android.app.PendingIntent;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProvider;
@@ -28,6 +29,7 @@
 import android.widget.RemoteViews;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry;
 import com.android.gallery3d.onetimeinitializer.GalleryWidgetMigrator;
 
@@ -50,8 +52,11 @@
     @Override
     public void onUpdate(Context context,
             AppWidgetManager appWidgetManager, int[] appWidgetIds) {
-        // migrate gallery widgets from pre-JB releases to JB due to bucket ID change
-        GalleryWidgetMigrator.migrateGalleryWidgets(context);
+
+        if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) {
+            // migrate gallery widgets from pre-JB releases to JB due to bucket ID change
+            GalleryWidgetMigrator.migrateGalleryWidgets(context);
+        }
 
         WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context);
         try {
@@ -71,6 +76,7 @@
     }
 
     @SuppressWarnings("deprecation")
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
     private static RemoteViews buildStackWidget(Context context, int widgetId, Entry entry) {
         RemoteViews views = new RemoteViews(
                 context.getPackageName(), R.layout.appwidget_main);
diff --git a/src/com/android/gallery3d/gadget/WidgetClickHandler.java b/src/com/android/gallery3d/gadget/WidgetClickHandler.java
index 35f9bec..37ee1a6 100644
--- a/src/com/android/gallery3d/gadget/WidgetClickHandler.java
+++ b/src/com/android/gallery3d/gadget/WidgetClickHandler.java
@@ -16,10 +16,12 @@
 
 package com.android.gallery3d.gadget;
 
+import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.Intent;
 import android.content.res.AssetFileDescriptor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
 import android.widget.Toast;
@@ -27,6 +29,7 @@
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.Gallery;
 import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.ApiHelper;
 
 public class WidgetClickHandler extends Activity {
     private static final String TAG = "PhotoAppWidgetClickHandler";
@@ -45,20 +48,29 @@
     }
 
     @Override
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
     protected void onCreate(Bundle savedState) {
         super.onCreate(savedState);
+        // The behavior is changed in JB, refer to b/6384492 for more details
+        boolean tediousBack = Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.JELLY_BEAN;
         Uri uri = getIntent().getData();
         Intent intent;
         if (isValidDataUri(uri)) {
             intent = new Intent(Intent.ACTION_VIEW, uri);
-            intent.putExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
+            if (tediousBack) {
+                intent.putExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
+            }
         } else {
             Toast.makeText(this,
                     R.string.no_such_item, Toast.LENGTH_LONG).show();
             intent = new Intent(this, Gallery.class);
         }
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
-                Intent.FLAG_ACTIVITY_TASK_ON_HOME);
+        if (tediousBack) {
+            intent.setFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK |
+                    Intent.FLAG_ACTIVITY_CLEAR_TASK |
+                    Intent.FLAG_ACTIVITY_TASK_ON_HOME);
+        }
         startActivity(intent);
         finish();
     }
diff --git a/src/com/android/gallery3d/gadget/WidgetConfigure.java b/src/com/android/gallery3d/gadget/WidgetConfigure.java
index 5717657..331e7d2 100644
--- a/src/com/android/gallery3d/gadget/WidgetConfigure.java
+++ b/src/com/android/gallery3d/gadget/WidgetConfigure.java
@@ -29,12 +29,14 @@
 import com.android.gallery3d.app.AlbumPicker;
 import com.android.gallery3d.app.CropImage;
 import com.android.gallery3d.app.DialogPicker;
+import com.android.gallery3d.common.ApiHelper;
 
 public class WidgetConfigure extends Activity {
     @SuppressWarnings("unused")
     private static final String TAG = "WidgetConfigure";
 
     public static final String KEY_WIDGET_TYPE = "widget-type";
+    private static final String KEY_PICKED_ITEM = "picked-item";
 
     private static final int REQUEST_WIDGET_TYPE = 1;
     private static final int REQUEST_CHOOSE_ALBUM = 2;
@@ -51,12 +53,11 @@
     private static int MAX_WIDGET_SIDE = 360;
 
     private int mAppWidgetId = -1;
-    private int mWidgetType = 0;
     private Uri mPickedItem;
 
     @Override
-    protected void onCreate(Bundle bundle) {
-        super.onCreate(bundle);
+    protected void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
         mAppWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
 
         if (mAppWidgetId == -1) {
@@ -65,12 +66,24 @@
             return;
         }
 
-        if (mWidgetType == 0) {
-            Intent intent = new Intent(this, WidgetTypeChooser.class);
-            startActivityForResult(intent, REQUEST_WIDGET_TYPE);
+        if (savedState == null) {
+            if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) {
+                Intent intent = new Intent(this, WidgetTypeChooser.class);
+                startActivityForResult(intent, REQUEST_WIDGET_TYPE);
+            } else { // Choose the photo type widget
+                setWidgetType(new Intent()
+                        .putExtra(KEY_WIDGET_TYPE, R.id.widget_type_photo));
+            }
+        } else {
+            mPickedItem = savedState.getParcelable(KEY_PICKED_ITEM);
         }
     }
 
+    protected void onSaveInstanceStates(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putParcelable(KEY_PICKED_ITEM, mPickedItem);
+    }
+
     private void updateWidgetAndFinish(WidgetDatabaseHelper.Entry entry) {
         AppWidgetManager manager = AppWidgetManager.getInstance(this);
         RemoteViews views = PhotoAppWidgetProvider.buildWidget(this, mAppWidgetId, entry);
@@ -153,11 +166,11 @@
     }
 
     private void setWidgetType(Intent data) {
-        mWidgetType = data.getIntExtra(KEY_WIDGET_TYPE, R.id.widget_type_shuffle);
-        if (mWidgetType == R.id.widget_type_album) {
+        int widgetType = data.getIntExtra(KEY_WIDGET_TYPE, R.id.widget_type_shuffle);
+        if (widgetType == R.id.widget_type_album) {
             Intent intent = new Intent(this, AlbumPicker.class);
             startActivityForResult(intent, REQUEST_CHOOSE_ALBUM);
-        } else if (mWidgetType == R.id.widget_type_shuffle) {
+        } else if (widgetType == R.id.widget_type_shuffle) {
             WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
             try {
                 helper.setWidget(mAppWidgetId, WidgetDatabaseHelper.TYPE_SHUFFLE, null);
diff --git a/src/com/android/gallery3d/gadget/WidgetService.java b/src/com/android/gallery3d/gadget/WidgetService.java
index eba7403..83e37a6 100644
--- a/src/com/android/gallery3d/gadget/WidgetService.java
+++ b/src/com/android/gallery3d/gadget/WidgetService.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.gadget;
 
+import android.annotation.TargetApi;
 import android.appwidget.AppWidgetManager;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -25,11 +26,13 @@
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.data.ContentListener;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.Path;
 
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
 public class WidgetService extends RemoteViewsService {
 
     @SuppressWarnings("unused")
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
index 2a0157a..190b2b8 100644
--- a/src/com/android/gallery3d/ui/ActionModeHandler.java
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -227,10 +227,9 @@
         return operation;
     }
 
-    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
     private void setNfcBeamPushUris(Uri[] uris) {
-        if (mNfcAdapter != null &&
-                Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.JELLY_BEAN) {
+        if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
             mNfcAdapter.setBeamPushUris(uris, (Activity)mActivity);
         }
     }
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
index 6561a23..449c6f9 100644
--- a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
+++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
@@ -23,6 +23,7 @@
 import com.android.gallery3d.app.AlbumSetDataLoader;
 import com.android.gallery3d.app.GalleryActivity;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BitmapPool;
 import com.android.gallery3d.data.DataSourceType;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
@@ -423,7 +424,8 @@
 
         @Override
         protected void recycleBitmap(Bitmap bitmap) {
-            MediaItem.getMicroThumbPool().recycle(bitmap);
+            BitmapPool pool = MediaItem.getMicroThumbPool();
+            if (pool != null) pool.recycle(bitmap);
         }
 
         @Override
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
index deec171..d5d90a9 100644
--- a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -22,6 +22,7 @@
 import com.android.gallery3d.app.AlbumDataLoader;
 import com.android.gallery3d.app.GalleryActivity;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BitmapPool;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.Path;
 import com.android.gallery3d.util.Future;
@@ -277,7 +278,8 @@
 
         @Override
         protected void recycleBitmap(Bitmap bitmap) {
-            MediaItem.getMicroThumbPool().recycle(bitmap);
+            BitmapPool pool = MediaItem.getMicroThumbPool();
+            if (pool != null) pool.recycle(bitmap);
         }
 
         @Override
diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
index 5c91757..0043e0c 100644
--- a/src/com/android/gallery3d/ui/BitmapScreenNail.java
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -20,6 +20,7 @@
 import android.graphics.RectF;
 
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BitmapPool;
 import com.android.gallery3d.data.MediaItem;
 
 // This is a ScreenNail wraps a Bitmap. There are some extra functions:
@@ -70,6 +71,11 @@
         mHeight = Math.round(scale * height);
     }
 
+    private static void recycleBitmap(BitmapPool pool, Bitmap bitmap) {
+        if (pool == null || bitmap == null) return;
+        pool.recycle(bitmap);
+    }
+
     // Combines the two ScreenNails.
     // Returns the used one and recycle the unused one.
     public ScreenNail combine(ScreenNail other) {
@@ -88,9 +94,7 @@
         mWidth = newer.mWidth;
         mHeight = newer.mHeight;
         if (newer.mBitmap != null) {
-            if (mBitmap != null) {
-                MediaItem.getThumbPool().recycle(mBitmap);
-            }
+            recycleBitmap(MediaItem.getThumbPool(), mBitmap);
             mBitmap = newer.mBitmap;
             newer.mBitmap = null;
 
@@ -130,10 +134,8 @@
             mTexture.recycle();
             mTexture = null;
         }
-        if (mBitmap != null) {
-            MediaItem.getThumbPool().recycle(mBitmap);
-            mBitmap = null;
-        }
+        recycleBitmap(MediaItem.getThumbPool(), mBitmap);
+        mBitmap = null;
     }
 
     @Override
diff --git a/src/com/android/gallery3d/ui/CustomMenu.java b/src/com/android/gallery3d/ui/CustomMenu.java
index dd8e6ab..15b3dc2 100644
--- a/src/com/android/gallery3d/ui/CustomMenu.java
+++ b/src/com/android/gallery3d/ui/CustomMenu.java
@@ -25,8 +25,6 @@
 import android.widget.PopupMenu;
 import android.widget.PopupMenu.OnMenuItemClickListener;
 
-import com.android.gallery3d.R;
-
 import java.util.ArrayList;
 
 public class CustomMenu implements OnMenuItemClickListener {
@@ -46,6 +44,7 @@
             mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
             mPopupMenu.setOnMenuItemClickListener(listener);
             mButton.setOnClickListener(new OnClickListener() {
+                @Override
                 public void onClick(View v) {
                     mPopupMenu.show();
                 }
@@ -61,8 +60,6 @@
         }
     }
 
-
-
     private Context mContext;
     private ArrayList<DropDownMenu> mMenus;
     private OnMenuItemClickListener mListener;
@@ -82,6 +79,7 @@
         mListener = listener;
     }
 
+    @Override
     public boolean onMenuItemClick(MenuItem item) {
         if (mListener != null) {
             return mListener.onMenuItemClick(item);
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java
index 478cb4f..9fd9473 100644
--- a/src/com/android/gallery3d/ui/GLRootView.java
+++ b/src/com/android/gallery3d/ui/GLRootView.java
@@ -21,6 +21,7 @@
 import android.graphics.Matrix;
 import android.graphics.PixelFormat;
 import android.opengl.GLSurfaceView;
+import android.os.Build;
 import android.os.Process;
 import android.os.SystemClock;
 import android.util.AttributeSet;
@@ -84,9 +85,6 @@
     private Matrix mCompensationMatrix = new Matrix();
     private int mDisplayRotation;
 
-    // The value which will become mCompensation in next layout.
-    private int mPendingCompensation;
-
     private int mFlags = FLAG_NEED_LAYOUT;
     private volatile boolean mRenderRequested = false;
 
@@ -333,6 +331,7 @@
         if (mFirstDraw) {
             mFirstDraw = false;
             post(new Runnable() {
+                    @Override
                     public void run() {
                         View root = getRootView();
                         View cover = root.findViewById(R.id.gl_root_cover);
@@ -538,11 +537,13 @@
     }
 
     @Override
-    @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     public void setLightsOutMode(boolean enabled) {
+        if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return;
+
         int flags = 0;
         if (enabled) {
-            flags = SYSTEM_UI_FLAG_LOW_PROFILE;
+            flags = STATUS_BAR_HIDDEN;
             if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
                 flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE);
             }
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
index 6d33470..9b77d68 100644
--- a/src/com/android/gallery3d/ui/PositionController.java
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -19,8 +19,8 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.Log;
-import android.widget.OverScroller;
 
+import com.android.gallery3d.common.OverScroller;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.RangeArray;
diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
index 5f45f16..dd9b7ce 100644
--- a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
+++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
@@ -16,13 +16,20 @@
 
 package com.android.gallery3d.ui;
 
+import android.annotation.TargetApi;
 import android.graphics.RectF;
 import android.graphics.SurfaceTexture;
 import android.opengl.GLES11Ext;
 
+import com.android.gallery3d.common.ApiHelper;
+
 public abstract class SurfaceTextureScreenNail implements ScreenNail,
         SurfaceTexture.OnFrameAvailableListener {
     private static final String TAG = "SurfaceTextureScreenNail";
+    // This constant is not available in API level before 15, but it was just an
+    // oversight.
+    private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
+
     protected ExtTexture mExtTexture;
     private SurfaceTexture mSurfaceTexture;
     private int mWidth, mHeight;
@@ -33,16 +40,30 @@
     }
 
     public void acquireSurfaceTexture() {
-        mExtTexture = new ExtTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
+        mExtTexture = new ExtTexture(GL_TEXTURE_EXTERNAL_OES);
         mExtTexture.setSize(mWidth, mHeight);
         mSurfaceTexture = new SurfaceTexture(mExtTexture.getId());
-        mSurfaceTexture.setDefaultBufferSize(mWidth, mHeight);
+        setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight);
         mSurfaceTexture.setOnFrameAvailableListener(this);
         synchronized (this) {
             mHasTexture = true;
         }
     }
 
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+    private static void setDefaultBufferSize(SurfaceTexture st, int width, int height) {
+        if (ApiHelper.HAS_SET_DEFALT_BUFFER_SIZE) {
+            st.setDefaultBufferSize(width, height);
+        }
+    }
+
+    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private static void releaseSurfaceTexture(SurfaceTexture st) {
+        if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) {
+            st.release();
+        }
+    }
+
     public SurfaceTexture getSurfaceTexture() {
         return mSurfaceTexture;
     }
@@ -53,7 +74,7 @@
         }
         mExtTexture.recycle();
         mExtTexture = null;
-        mSurfaceTexture.release();
+        releaseSurfaceTexture(mSurfaceTexture);
         mSurfaceTexture = null;
     }
 
diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java
index 1291ee9..05198f4 100644
--- a/src/com/android/gallery3d/util/GalleryUtils.java
+++ b/src/com/android/gallery3d/util/GalleryUtils.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.util;
 
+import android.annotation.TargetApi;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
 import android.content.Context;
@@ -35,6 +36,7 @@
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.PackagesMonitor;
+import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.util.ThreadPool.CancelListener;
@@ -288,6 +290,7 @@
         return durationValue;
     }
 
+    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
     public static int determineTypeBits(Context context, Intent intent) {
         int typeBits = 0;
         String type = intent.resolveType(context);
@@ -304,8 +307,10 @@
             typeBits = DataManager.INCLUDE_ALL;
         }
 
-        if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) {
-            typeBits |= DataManager.INCLUDE_LOCAL_ONLY;
+        if (ApiHelper.HAS_INTENT_EXTRA_LOCAL_ONLY) {
+            if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) {
+                typeBits |= DataManager.INCLUDE_LOCAL_ONLY;
+            }
         }
 
         return typeBits;