Merge "Initial checkin of spot presentation for touchpad gestures."
diff --git a/core/java/android/view/PointerIcon.aidl b/core/java/android/view/PointerIcon.aidl
new file mode 100644
index 0000000..b09340b
--- /dev/null
+++ b/core/java/android/view/PointerIcon.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+parcelable PointerIcon;
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
new file mode 100644
index 0000000..bb7ed41
--- /dev/null
+++ b/core/java/android/view/PointerIcon.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.internal.util.XmlUtils;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+/**
+ * Represents an icon that can be used as a mouse pointer.
+ * <p>
+ * Pointer icons can be provided either by the system using system styles,
+ * or by applications using bitmaps or application resources.
+ * </p>
+ *
+ * @hide
+ */
+public final class PointerIcon implements Parcelable {
+    private static final String TAG = "PointerIcon";
+
+    /** Style constant: Custom icon with a user-supplied bitmap. */
+    public static final int STYLE_CUSTOM = -1;
+
+    /** Style constant: Null icon.  It has no bitmap. */
+    public static final int STYLE_NULL = 0;
+
+    /** Style constant: Arrow icon.  (Default mouse pointer) */
+    public static final int STYLE_ARROW = 1000;
+
+    /** {@hide} Style constant: Spot hover icon for touchpads. */
+    public static final int STYLE_SPOT_HOVER = 2000;
+
+    /** {@hide} Style constant: Spot touch icon for touchpads. */
+    public static final int STYLE_SPOT_TOUCH = 2001;
+
+    /** {@hide} Style constant: Spot anchor icon for touchpads. */
+    public static final int STYLE_SPOT_ANCHOR = 2002;
+
+    // OEM private styles should be defined starting at this range to avoid
+    // conflicts with any system styles that may be defined in the future.
+    private static final int STYLE_OEM_FIRST = 10000;
+
+    // The default pointer icon.
+    private static final int STYLE_DEFAULT = STYLE_ARROW;
+
+    private static final PointerIcon gNullIcon = new PointerIcon(STYLE_NULL);
+
+    private final int mStyle;
+    private int mSystemIconResourceId;
+    private Bitmap mBitmap;
+    private float mHotSpotX;
+    private float mHotSpotY;
+
+    private PointerIcon(int style) {
+        mStyle = style;
+    }
+
+    /**
+     * Gets a special pointer icon that has no bitmap.
+     *
+     * @return The null pointer icon.
+     *
+     * @see #STYLE_NULL
+     */
+    public static PointerIcon getNullIcon() {
+        return gNullIcon;
+    }
+
+    /**
+     * Gets the default pointer icon.
+     *
+     * @param context The context.
+     * @return The default pointer icon.
+     *
+     * @throws IllegalArgumentException if context is null.
+     */
+    public static PointerIcon getDefaultIcon(Context context) {
+        return getSystemIcon(context, STYLE_DEFAULT);
+    }
+
+    /**
+     * Gets a system pointer icon for the given style.
+     * If style is not recognized, returns the default pointer icon.
+     *
+     * @param context The context.
+     * @param style The pointer icon style.
+     * @return The pointer icon.
+     *
+     * @throws IllegalArgumentException if context is null.
+     */
+    public static PointerIcon getSystemIcon(Context context, int style) {
+        if (context == null) {
+            throw new IllegalArgumentException("context must not be null");
+        }
+
+        if (style == STYLE_NULL) {
+            return gNullIcon;
+        }
+
+        int styleIndex = getSystemIconStyleIndex(style);
+        if (styleIndex == 0) {
+            styleIndex = getSystemIconStyleIndex(STYLE_DEFAULT);
+        }
+
+        TypedArray a = context.obtainStyledAttributes(null,
+                com.android.internal.R.styleable.Pointer,
+                com.android.internal.R.attr.pointerStyle, 0);
+        int resourceId = a.getResourceId(styleIndex, -1);
+        a.recycle();
+
+        if (resourceId == -1) {
+            Log.w(TAG, "Missing theme resources for pointer icon style " + style);
+            return style == STYLE_DEFAULT ? gNullIcon : getSystemIcon(context, STYLE_DEFAULT);
+        }
+
+        PointerIcon icon = new PointerIcon(style);
+        if ((resourceId & 0xff000000) == 0x01000000) {
+            icon.mSystemIconResourceId = resourceId;
+        } else {
+            icon.loadResource(context.getResources(), resourceId);
+        }
+        return icon;
+    }
+
+    /**
+     * Creates a custom pointer from the given bitmap and hotspot information.
+     *
+     * @param bitmap The bitmap for the icon.
+     * @param hotspotX The X offset of the pointer icon hotspot in the bitmap.
+     *        Must be within the [0, bitmap.getWidth()) range.
+     * @param hotspotY The Y offset of the pointer icon hotspot in the bitmap.
+     *        Must be within the [0, bitmap.getHeight()) range.
+     * @return A pointer icon for this bitmap.
+     *
+     * @throws IllegalArgumentException if bitmap is null, or if the x/y hotspot
+     *         parameters are invalid.
+     */
+    public static PointerIcon createCustomIcon(Bitmap bitmap, float hotSpotX, float hotSpotY) {
+        if (bitmap == null) {
+            throw new IllegalArgumentException("bitmap must not be null");
+        }
+        validateHotSpot(bitmap, hotSpotX, hotSpotY);
+
+        PointerIcon icon = new PointerIcon(STYLE_CUSTOM);
+        icon.mBitmap = bitmap;
+        icon.mHotSpotX = hotSpotX;
+        icon.mHotSpotY = hotSpotY;
+        return icon;
+    }
+
+    /**
+     * Loads a custom pointer icon from an XML resource.
+     * <p>
+     * The XML resource should have the following form:
+     * <code>
+     * &lt;?xml version="1.0" encoding="utf-8"?&gt;
+     * &lt;pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
+     *   android:bitmap="@drawable/my_pointer_bitmap"
+     *   android:hotSpotX="24"
+     *   android:hotSpotY="24" /&gt;
+     * </code>
+     * </p>
+     *
+     * @param resources The resources object.
+     * @param resourceId The resource id.
+     * @return The pointer icon.
+     *
+     * @throws IllegalArgumentException if resources is null.
+     * @throws Resources.NotFoundException if the resource was not found or the drawable
+     * linked in the resource was not found.
+     */
+    public static PointerIcon loadCustomIcon(Resources resources, int resourceId) {
+        if (resources == null) {
+            throw new IllegalArgumentException("resources must not be null");
+        }
+
+        PointerIcon icon = new PointerIcon(STYLE_CUSTOM);
+        icon.loadResource(resources, resourceId);
+        return icon;
+    }
+
+    /**
+     * Loads the bitmap and hotspot information for a pointer icon, if it is not already loaded.
+     * Returns a pointer icon (not necessarily the same instance) with the information filled in.
+     *
+     * @param context The context.
+     * @return The loaded pointer icon.
+     *
+     * @throws IllegalArgumentException if context is null.
+     * @see #isLoaded()
+     * @hide
+     */
+    public PointerIcon load(Context context) {
+        if (context == null) {
+            throw new IllegalArgumentException("context must not be null");
+        }
+
+        if (mSystemIconResourceId == 0 || mBitmap != null) {
+            return this;
+        }
+
+        PointerIcon result = new PointerIcon(mStyle);
+        result.mSystemIconResourceId = mSystemIconResourceId;
+        result.loadResource(context.getResources(), mSystemIconResourceId);
+        return result;
+    }
+
+    /**
+     * Returns true if the pointer icon style is {@link #STYLE_NULL}.
+     *
+     * @return True if the pointer icon style is {@link #STYLE_NULL}.
+     */
+    public boolean isNullIcon() {
+        return mStyle == STYLE_NULL;
+    }
+
+    /**
+     * Returns true if the pointer icon has been loaded and its bitmap and hotspot
+     * information are available.
+     *
+     * @return True if the pointer icon is loaded.
+     * @see #load(Context)
+     */
+    public boolean isLoaded() {
+        return mBitmap != null || mStyle == STYLE_NULL;
+    }
+
+    /**
+     * Gets the style of the pointer icon.
+     *
+     * @return The pointer icon style.
+     */
+    public int getStyle() {
+        return mStyle;
+    }
+
+    /**
+     * Gets the bitmap of the pointer icon.
+     *
+     * @return The pointer icon bitmap, or null if the style is {@link #STYLE_NULL}.
+     *
+     * @throws IllegalStateException if the bitmap is not loaded.
+     * @see #isLoaded()
+     * @see #load(Context)
+     */
+    public Bitmap getBitmap() {
+        throwIfIconIsNotLoaded();
+        return mBitmap;
+    }
+
+    /**
+     * Gets the X offset of the pointer icon hotspot.
+     *
+     * @return The hotspot X offset.
+     *
+     * @throws IllegalStateException if the bitmap is not loaded.
+     * @see #isLoaded()
+     * @see #load(Context)
+     */
+    public float getHotSpotX() {
+        throwIfIconIsNotLoaded();
+        return mHotSpotX;
+    }
+
+    /**
+     * Gets the Y offset of the pointer icon hotspot.
+     *
+     * @return The hotspot Y offset.
+     *
+     * @throws IllegalStateException if the bitmap is not loaded.
+     * @see #isLoaded()
+     * @see #load(Context)
+     */
+    public float getHotSpotY() {
+        throwIfIconIsNotLoaded();
+        return mHotSpotY;
+    }
+
+    private void throwIfIconIsNotLoaded() {
+        if (!isLoaded()) {
+            throw new IllegalStateException("The icon is not loaded.");
+        }
+    }
+
+    public static final Parcelable.Creator<PointerIcon> CREATOR
+            = new Parcelable.Creator<PointerIcon>() {
+        public PointerIcon createFromParcel(Parcel in) {
+            int style = in.readInt();
+            if (style == STYLE_NULL) {
+                return getNullIcon();
+            }
+
+            int systemIconResourceId = in.readInt();
+            if (systemIconResourceId != 0) {
+                PointerIcon icon = new PointerIcon(style);
+                icon.mSystemIconResourceId = systemIconResourceId;
+                return icon;
+            }
+
+            Bitmap bitmap = Bitmap.CREATOR.createFromParcel(in);
+            float hotSpotX = in.readFloat();
+            float hotSpotY = in.readFloat();
+            return PointerIcon.createCustomIcon(bitmap, hotSpotX, hotSpotY);
+        }
+
+        public PointerIcon[] newArray(int size) {
+            return new PointerIcon[size];
+        }
+    };
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mStyle);
+
+        if (mStyle != STYLE_NULL) {
+            out.writeInt(mSystemIconResourceId);
+            if (mSystemIconResourceId == 0) {
+                mBitmap.writeToParcel(out, flags);
+                out.writeFloat(mHotSpotX);
+                out.writeFloat(mHotSpotY);
+            }
+        }
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other == null || !(other instanceof PointerIcon)) {
+            return false;
+        }
+
+        PointerIcon otherIcon = (PointerIcon) other;
+        if (mStyle != otherIcon.mStyle
+                || mSystemIconResourceId != otherIcon.mSystemIconResourceId) {
+            return false;
+        }
+
+        if (mSystemIconResourceId == 0 && (mBitmap != otherIcon.mBitmap
+                || mHotSpotX != otherIcon.mHotSpotX
+                || mHotSpotY != otherIcon.mHotSpotY)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private void loadResource(Resources resources, int resourceId) {
+        XmlResourceParser parser = resources.getXml(resourceId);
+        final int bitmapRes;
+        final float hotSpotX;
+        final float hotSpotY;
+        try {
+            XmlUtils.beginDocument(parser, "pointer-icon");
+
+            TypedArray a = resources.obtainAttributes(
+                    parser, com.android.internal.R.styleable.PointerIcon);
+            bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
+            hotSpotX = a.getFloat(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
+            hotSpotY = a.getFloat(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
+            a.recycle();
+        } catch (Exception ex) {
+            throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex);
+        } finally {
+            parser.close();
+        }
+
+        if (bitmapRes == 0) {
+            throw new IllegalArgumentException("<pointer-icon> is missing bitmap attribute.");
+        }
+
+        Drawable drawable = resources.getDrawable(bitmapRes);
+        if (!(drawable instanceof BitmapDrawable)) {
+            throw new IllegalArgumentException("<pointer-icon> bitmap attribute must "
+                    + "refer to a bitmap drawable.");
+        }
+
+        // Set the properties now that we have successfully loaded the icon.
+        mBitmap = ((BitmapDrawable)drawable).getBitmap();
+        mHotSpotX = hotSpotX;
+        mHotSpotY = hotSpotY;
+    }
+
+    private static void validateHotSpot(Bitmap bitmap, float hotSpotX, float hotSpotY) {
+        if (hotSpotX < 0 || hotSpotX >= bitmap.getWidth()) {
+            throw new IllegalArgumentException("x hotspot lies outside of the bitmap area");
+        }
+        if (hotSpotY < 0 || hotSpotY >= bitmap.getHeight()) {
+            throw new IllegalArgumentException("y hotspot lies outside of the bitmap area");
+        }
+    }
+
+    private static int getSystemIconStyleIndex(int style) {
+        switch (style) {
+            case STYLE_ARROW:
+                return com.android.internal.R.styleable.Pointer_pointerIconArrow;
+            case STYLE_SPOT_HOVER:
+                return com.android.internal.R.styleable.Pointer_pointerIconSpotHover;
+            case STYLE_SPOT_TOUCH:
+                return com.android.internal.R.styleable.Pointer_pointerIconSpotTouch;
+            case STYLE_SPOT_ANCHOR:
+                return com.android.internal.R.styleable.Pointer_pointerIconSpotAnchor;
+            default:
+                return 0;
+        }
+    }
+}
diff --git a/core/jni/Android.mk b/core/jni/Android.mk
index f8f8761..290f528 100644
--- a/core/jni/Android.mk
+++ b/core/jni/Android.mk
@@ -54,6 +54,7 @@
 	android_view_KeyCharacterMap.cpp \
 	android_view_GLES20Canvas.cpp \
 	android_view_MotionEvent.cpp \
+	android_view_PointerIcon.cpp \
 	android_view_VelocityTracker.cpp \
 	android_text_AndroidCharacter.cpp \
 	android_text_AndroidBidi.cpp \
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index b628b9dc..a4a229a 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -170,6 +170,7 @@
 extern int register_android_view_InputQueue(JNIEnv* env);
 extern int register_android_view_KeyEvent(JNIEnv* env);
 extern int register_android_view_MotionEvent(JNIEnv* env);
+extern int register_android_view_PointerIcon(JNIEnv* env);
 extern int register_android_view_VelocityTracker(JNIEnv* env);
 extern int register_android_content_res_ObbScanner(JNIEnv* env);
 extern int register_android_content_res_Configuration(JNIEnv* env);
@@ -1212,6 +1213,7 @@
     REG_JNI(register_android_view_InputQueue),
     REG_JNI(register_android_view_KeyEvent),
     REG_JNI(register_android_view_MotionEvent),
+    REG_JNI(register_android_view_PointerIcon),
     REG_JNI(register_android_view_VelocityTracker),
 
     REG_JNI(register_android_content_res_ObbScanner),
diff --git a/core/jni/android_view_PointerIcon.cpp b/core/jni/android_view_PointerIcon.cpp
new file mode 100644
index 0000000..091341a
--- /dev/null
+++ b/core/jni/android_view_PointerIcon.cpp
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "PointerIcon-JNI"
+
+#include "JNIHelp.h"
+
+#include "android_view_PointerIcon.h"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <utils/Log.h>
+#include <android/graphics/GraphicsJNI.h>
+
+namespace android {
+
+static struct {
+    jclass clazz;
+    jfieldID mStyle;
+    jfieldID mBitmap;
+    jfieldID mHotSpotX;
+    jfieldID mHotSpotY;
+    jmethodID getSystemIcon;
+    jmethodID load;
+} gPointerIconClassInfo;
+
+
+// --- Global Functions ---
+
+jobject android_view_PointerIcon_getSystemIcon(JNIEnv* env, jobject contextObj, int32_t style) {
+    jobject pointerIconObj = env->CallStaticObjectMethod(gPointerIconClassInfo.clazz,
+            gPointerIconClassInfo.getSystemIcon, contextObj, style);
+    if (env->ExceptionCheck()) {
+        LOGW("An exception occurred while getting a pointer icon with style %d.", style);
+        LOGW_EX(env);
+        env->ExceptionClear();
+        return NULL;
+    }
+    return pointerIconObj;
+}
+
+status_t android_view_PointerIcon_load(JNIEnv* env, jobject pointerIconObj, jobject contextObj,
+        PointerIcon* outPointerIcon) {
+    outPointerIcon->reset();
+
+    if (!pointerIconObj) {
+        return OK;
+    }
+
+    jobject loadedPointerIconObj = env->CallObjectMethod(pointerIconObj,
+            gPointerIconClassInfo.load, contextObj);
+    if (env->ExceptionCheck() || !loadedPointerIconObj) {
+        LOGW("An exception occurred while loading a pointer icon.");
+        LOGW_EX(env);
+        env->ExceptionClear();
+        return UNKNOWN_ERROR;
+    }
+
+    outPointerIcon->style = env->GetIntField(loadedPointerIconObj,
+            gPointerIconClassInfo.mStyle);
+    outPointerIcon->hotSpotX = env->GetFloatField(loadedPointerIconObj,
+            gPointerIconClassInfo.mHotSpotX);
+    outPointerIcon->hotSpotY = env->GetFloatField(loadedPointerIconObj,
+            gPointerIconClassInfo.mHotSpotY);
+
+    jobject bitmapObj = env->GetObjectField(loadedPointerIconObj, gPointerIconClassInfo.mBitmap);
+    if (bitmapObj) {
+        SkBitmap* bitmap = GraphicsJNI::getNativeBitmap(env, bitmapObj);
+        if (bitmap) {
+            outPointerIcon->bitmap = *bitmap; // use a shared pixel ref
+        }
+        env->DeleteLocalRef(bitmapObj);
+    }
+
+    env->DeleteLocalRef(loadedPointerIconObj);
+    return OK;
+}
+
+status_t android_view_PointerIcon_loadSystemIcon(JNIEnv* env, jobject contextObj,
+        int32_t style, PointerIcon* outPointerIcon) {
+    jobject pointerIconObj = android_view_PointerIcon_getSystemIcon(env, contextObj, style);
+    if (!pointerIconObj) {
+        outPointerIcon->reset();
+        return UNKNOWN_ERROR;
+    }
+
+    status_t status = android_view_PointerIcon_load(env, pointerIconObj,
+            contextObj, outPointerIcon);
+    env->DeleteLocalRef(pointerIconObj);
+    return status;
+}
+
+
+// --- JNI Registration ---
+
+#define FIND_CLASS(var, className) \
+        var = env->FindClass(className); \
+        LOG_FATAL_IF(! var, "Unable to find class " className); \
+        var = jclass(env->NewGlobalRef(var));
+
+#define GET_STATIC_METHOD_ID(var, clazz, methodName, methodDescriptor) \
+        var = env->GetStaticMethodID(clazz, methodName, methodDescriptor); \
+        LOG_FATAL_IF(! var, "Unable to find method " methodName);
+
+#define GET_METHOD_ID(var, clazz, methodName, methodDescriptor) \
+        var = env->GetMethodID(clazz, methodName, methodDescriptor); \
+        LOG_FATAL_IF(! var, "Unable to find method " methodName);
+
+#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \
+        var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \
+        LOG_FATAL_IF(! var, "Unable to find field " fieldName);
+
+int register_android_view_PointerIcon(JNIEnv* env) {
+    FIND_CLASS(gPointerIconClassInfo.clazz, "android/view/PointerIcon");
+
+    GET_FIELD_ID(gPointerIconClassInfo.mBitmap, gPointerIconClassInfo.clazz,
+            "mBitmap", "Landroid/graphics/Bitmap;");
+
+    GET_FIELD_ID(gPointerIconClassInfo.mStyle, gPointerIconClassInfo.clazz,
+            "mStyle", "I");
+
+    GET_FIELD_ID(gPointerIconClassInfo.mHotSpotX, gPointerIconClassInfo.clazz,
+            "mHotSpotX", "F");
+
+    GET_FIELD_ID(gPointerIconClassInfo.mHotSpotY, gPointerIconClassInfo.clazz,
+            "mHotSpotY", "F");
+
+    GET_STATIC_METHOD_ID(gPointerIconClassInfo.getSystemIcon, gPointerIconClassInfo.clazz,
+            "getSystemIcon", "(Landroid/content/Context;I)Landroid/view/PointerIcon;");
+
+    GET_METHOD_ID(gPointerIconClassInfo.load, gPointerIconClassInfo.clazz,
+            "load", "(Landroid/content/Context;)Landroid/view/PointerIcon;");
+
+    return 0;
+}
+
+} // namespace android
diff --git a/core/jni/android_view_PointerIcon.h b/core/jni/android_view_PointerIcon.h
new file mode 100644
index 0000000..3bfd645
--- /dev/null
+++ b/core/jni/android_view_PointerIcon.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef _ANDROID_VIEW_POINTER_ICON_H
+#define _ANDROID_VIEW_POINTER_ICON_H
+
+#include "jni.h"
+
+#include <utils/Errors.h>
+#include <SkBitmap.h>
+
+namespace android {
+
+/* Pointer icon styles.
+ * Must match the definition in android.view.PointerIcon.
+ */
+enum {
+    POINTER_ICON_STYLE_CUSTOM = -1,
+    POINTER_ICON_STYLE_NULL = 0,
+    POINTER_ICON_STYLE_ARROW = 1000,
+    POINTER_ICON_STYLE_SPOT_HOVER = 2000,
+    POINTER_ICON_STYLE_SPOT_TOUCH = 2001,
+    POINTER_ICON_STYLE_SPOT_ANCHOR = 2002,
+};
+
+/*
+ * Describes a pointer icon.
+ */
+struct PointerIcon {
+    inline PointerIcon() {
+        reset();
+    }
+
+    int32_t style;
+    SkBitmap bitmap;
+    float hotSpotX;
+    float hotSpotY;
+
+    inline bool isNullIcon() {
+        return style == POINTER_ICON_STYLE_NULL;
+    }
+
+    inline void reset() {
+        style = POINTER_ICON_STYLE_NULL;
+        bitmap.reset();
+        hotSpotX = 0;
+        hotSpotY = 0;
+    }
+};
+
+/* Gets a system pointer icon with the specified style. */
+extern jobject android_view_PointerIcon_getSystemIcon(JNIEnv* env,
+        jobject contextObj, int32_t style);
+
+/* Loads the bitmap associated with a pointer icon.
+ * If pointerIconObj is NULL, returns OK and a pointer icon with POINTER_ICON_STYLE_NULL. */
+extern status_t android_view_PointerIcon_load(JNIEnv* env,
+        jobject pointerIconObj, jobject contextObj, PointerIcon* outPointerIcon);
+
+/* Loads the bitmap associated with a pointer icon by style.
+ * If pointerIconObj is NULL, returns OK and a pointer icon with POINTER_ICON_STYLE_NULL. */
+extern status_t android_view_PointerIcon_loadSystemIcon(JNIEnv* env,
+        jobject contextObj, int32_t style, PointerIcon* outPointerIcon);
+
+} // namespace android
+
+#endif // _ANDROID_OS_POINTER_ICON_H
diff --git a/core/res/res/drawable-mdpi/pointer_spot_anchor.png b/core/res/res/drawable-mdpi/pointer_spot_anchor.png
new file mode 100644
index 0000000..d7aca36
--- /dev/null
+++ b/core/res/res/drawable-mdpi/pointer_spot_anchor.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/pointer_spot_anchor_icon.xml b/core/res/res/drawable-mdpi/pointer_spot_anchor_icon.xml
new file mode 100644
index 0000000..2222b8e
--- /dev/null
+++ b/core/res/res/drawable-mdpi/pointer_spot_anchor_icon.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
+    android:bitmap="@drawable/pointer_spot_anchor"
+    android:hotSpotX="33"
+    android:hotSpotY="33" />
diff --git a/core/res/res/drawable-mdpi/pointer_spot_hover.png b/core/res/res/drawable-mdpi/pointer_spot_hover.png
new file mode 100644
index 0000000..5041aa3
--- /dev/null
+++ b/core/res/res/drawable-mdpi/pointer_spot_hover.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/pointer_spot_hover_icon.xml b/core/res/res/drawable-mdpi/pointer_spot_hover_icon.xml
new file mode 100644
index 0000000..dc62a69
--- /dev/null
+++ b/core/res/res/drawable-mdpi/pointer_spot_hover_icon.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
+    android:bitmap="@drawable/pointer_spot_hover"
+    android:hotSpotX="33"
+    android:hotSpotY="33" />
diff --git a/core/res/res/drawable-mdpi/pointer_spot_touch.png b/core/res/res/drawable-mdpi/pointer_spot_touch.png
new file mode 100644
index 0000000..64a42a1
--- /dev/null
+++ b/core/res/res/drawable-mdpi/pointer_spot_touch.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/pointer_spot_touch_icon.xml b/core/res/res/drawable-mdpi/pointer_spot_touch_icon.xml
new file mode 100644
index 0000000..4bffee6
--- /dev/null
+++ b/core/res/res/drawable-mdpi/pointer_spot_touch_icon.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
+    android:bitmap="@drawable/pointer_spot_touch"
+    android:hotSpotX="24"
+    android:hotSpotY="24" />
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 819ce58..e8767d8 100755
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -757,6 +757,13 @@
         <!-- Default style for the Switch widget. -->
         <attr name="switchStyle" format="reference" />
 
+        <!-- ============== -->
+        <!-- Pointer styles -->
+        <!-- ============== -->
+        <eat-comment />
+
+        <!-- Reference to the Pointer style -->
+        <attr name="pointerStyle" format="reference" />
     </declare-styleable>
 
     <!-- **************************************************************** -->
@@ -4921,6 +4928,17 @@
         <attr name="switchPadding" format="dimension" />
     </declare-styleable>
 
+    <declare-styleable name="Pointer">
+        <!-- Reference to a pointer icon drawable with STYLE_ARROW -->
+        <attr name="pointerIconArrow" format="reference" />
+        <!-- Reference to a pointer icon drawable with STYLE_SPOT_HOVER -->
+        <attr name="pointerIconSpotHover" format="reference" />
+        <!-- Reference to a pointer icon drawable with STYLE_SPOT_TOUCH -->
+        <attr name="pointerIconSpotTouch" format="reference" />
+        <!-- Reference to a pointer icon drawable with STYLE_SPOT_ANCHOR -->
+        <attr name="pointerIconSpotAnchor" format="reference" />
+    </declare-styleable>
+
     <declare-styleable name="PointerIcon">
         <!-- Drawable to use as the icon bitmap. -->
         <attr name="bitmap" format="reference" />
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index bf4c6d7..b4042c0 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -2210,4 +2210,12 @@
         <item name="android:borderLeft">0dip</item>
         <item name="android:borderRight">0dip</item>
     </style>
+
+    <!-- Pointer styles -->
+    <style name="Pointer">
+        <item name="android:pointerIconArrow">@android:drawable/pointer_arrow_icon</item>
+        <item name="android:pointerIconSpotHover">@android:drawable/pointer_spot_hover_icon</item>
+        <item name="android:pointerIconSpotTouch">@android:drawable/pointer_spot_touch_icon</item>
+        <item name="android:pointerIconSpotAnchor">@android:drawable/pointer_spot_anchor_icon</item>
+    </style>
 </resources>
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index b1e4f0f..0748b10 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -323,6 +323,8 @@
         <item name="fastScrollOverlayPosition">floating</item>
         <item name="fastScrollTextColor">@android:color/primary_text_dark</item>
 
+        <!-- Pointer style -->
+        <item name="pointerStyle">@android:style/Pointer</item>
     </style>
 
     <!-- Variant of the default (dark) theme with no title bar -->
diff --git a/include/ui/Input.h b/include/ui/Input.h
index 0dc29c8..9b92c73 100644
--- a/include/ui/Input.h
+++ b/include/ui/Input.h
@@ -620,6 +620,11 @@
     // Oldest sample to consider when calculating the velocity.
     static const nsecs_t MAX_AGE = 200 * 1000000; // 200 ms
 
+    // When the total duration of the window of samples being averaged is less
+    // than the window size, the resulting velocity is scaled to reduce the impact
+    // of overestimation in short traces.
+    static const nsecs_t MIN_WINDOW = 100 * 1000000; // 100 ms
+
     // The minimum duration between samples when estimating velocity.
     static const nsecs_t MIN_DURATION = 10 * 1000000; // 10 ms
 
diff --git a/libs/ui/Input.cpp b/libs/ui/Input.cpp
index bbe579e..a95f432 100644
--- a/libs/ui/Input.cpp
+++ b/libs/ui/Input.cpp
@@ -832,6 +832,7 @@
         const Position& oldestPosition =
                 oldestMovement.positions[oldestMovement.idBits.getIndexOfBit(id)];
         nsecs_t lastDuration = 0;
+
         while (numTouches-- > 1) {
             if (++index == HISTORY_SIZE) {
                 index = 0;
@@ -858,6 +859,14 @@
 
         // Make sure we used at least one sample.
         if (samplesUsed != 0) {
+            // Scale the velocity linearly if the window of samples is small.
+            nsecs_t totalDuration = newestMovement.eventTime - oldestMovement.eventTime;
+            if (totalDuration < MIN_WINDOW) {
+                float scale = float(totalDuration) / float(MIN_WINDOW);
+                accumVx *= scale;
+                accumVy *= scale;
+            }
+
             *outVx = accumVx;
             *outVy = accumVy;
             return true;
diff --git a/libs/utils/Looper.cpp b/libs/utils/Looper.cpp
index d5dd126..b54fb9d 100644
--- a/libs/utils/Looper.cpp
+++ b/libs/utils/Looper.cpp
@@ -662,7 +662,8 @@
 #endif
 
 void Looper::sendMessage(const sp<MessageHandler>& handler, const Message& message) {
-    sendMessageAtTime(LLONG_MIN, handler, message);
+    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
+    sendMessageAtTime(now, handler, message);
 }
 
 void Looper::sendMessageDelayed(nsecs_t uptimeDelay, const sp<MessageHandler>& handler,
diff --git a/services/input/Android.mk b/services/input/Android.mk
index f9f8623..836c081 100644
--- a/services/input/Android.mk
+++ b/services/input/Android.mk
@@ -23,7 +23,6 @@
     InputReader.cpp \
     InputWindow.cpp \
     PointerController.cpp \
-    SpotController.cpp \
     SpriteController.cpp
 
 LOCAL_SHARED_LIBRARIES := \
diff --git a/services/input/InputReader.cpp b/services/input/InputReader.cpp
index 6db445e..1bc1bd1 100644
--- a/services/input/InputReader.cpp
+++ b/services/input/InputReader.cpp
@@ -36,7 +36,6 @@
 // Log debug messages about gesture detection.
 #define DEBUG_GESTURES 0
 
-
 #include "InputReader.h"
 
 #include <cutils/log.h>
@@ -71,23 +70,47 @@
 
 // Tap gesture delay time.
 // The time between down and up must be less than this to be considered a tap.
-static const nsecs_t TAP_INTERVAL = 100 * 1000000; // 100 ms
+static const nsecs_t TAP_INTERVAL = 150 * 1000000; // 150 ms
 
 // The distance in pixels that the pointer is allowed to move from initial down
 // to up and still be called a tap.
 static const float TAP_SLOP = 5.0f; // 5 pixels
 
-// The transition from INDETERMINATE_MULTITOUCH to SWIPE or FREEFORM gesture mode is made when
-// all of the pointers have traveled this number of pixels from the start point.
-static const float MULTITOUCH_MIN_TRAVEL = 5.0f;
+// Time after the first touch points go down to settle on an initial centroid.
+// This is intended to be enough time to handle cases where the user puts down two
+// fingers at almost but not quite exactly the same time.
+static const nsecs_t MULTITOUCH_SETTLE_INTERVAL = 100 * 1000000; // 100ms
 
-// The transition from INDETERMINATE_MULTITOUCH to SWIPE gesture mode can only occur when the
+// The transition from PRESS to SWIPE or FREEFORM gesture mode is made when
+// both of the pointers are moving at least this fast.
+static const float MULTITOUCH_MIN_SPEED = 150.0f; // pixels per second
+
+// The transition from PRESS to SWIPE gesture mode can only occur when the
 // cosine of the angle between the two vectors is greater than or equal to than this value
 // which indicates that the vectors are oriented in the same direction.
 // When the vectors are oriented in the exactly same direction, the cosine is 1.0.
 // (In exactly opposite directions, the cosine is -1.0.)
 static const float SWIPE_TRANSITION_ANGLE_COSINE = 0.5f; // cosine of 45 degrees
 
+// The transition from PRESS to SWIPE gesture mode can only occur when the
+// fingers are no more than this far apart relative to the diagonal size of
+// the touch pad.  For example, a ratio of 0.5 means that the fingers must be
+// no more than half the diagonal size of the touch pad apart.
+static const float SWIPE_MAX_WIDTH_RATIO = 0.333f; // 1/3
+
+// The gesture movement speed factor relative to the size of the display.
+// Movement speed applies when the fingers are moving in the same direction.
+// Without acceleration, a full swipe of the touch pad diagonal in movement mode
+// will cover this portion of the display diagonal.
+static const float GESTURE_MOVEMENT_SPEED_RATIO = 0.8f;
+
+// The gesture zoom speed factor relative to the size of the display.
+// Zoom speed applies when the fingers are mostly moving relative to each other
+// to execute a scale gesture or similar.
+// Without acceleration, a full swipe of the touch pad diagonal in zoom mode
+// will cover this portion of the display diagonal.
+static const float GESTURE_ZOOM_SPEED_RATIO = 0.3f;
+
 
 // --- Static Functions ---
 
@@ -112,14 +135,8 @@
     return (x + y) / 2;
 }
 
-inline static float pythag(float x, float y) {
-    return sqrtf(x * x + y * y);
-}
-
-inline static int32_t distanceSquared(int32_t x1, int32_t y1, int32_t x2, int32_t y2) {
-    int32_t dx = x1 - x2;
-    int32_t dy = y1 - y2;
-    return dx * dx + dy * dy;
+inline static float distance(float x1, float y1, float x2, float y2) {
+    return hypotf(x1 - x2, y1 - y2);
 }
 
 inline static int32_t signExtendNybble(int32_t value) {
@@ -224,6 +241,33 @@
     return edgeFlags;
 }
 
+static void clampPositionUsingPointerBounds(
+        const sp<PointerControllerInterface>& pointerController, float* x, float* y) {
+    float minX, minY, maxX, maxY;
+    if (pointerController->getBounds(&minX, &minY, &maxX, &maxY)) {
+        if (*x < minX) {
+            *x = minX;
+        } else if (*x > maxX) {
+            *x = maxX;
+        }
+        if (*y < minY) {
+            *y = minY;
+        } else if (*y > maxY) {
+            *y = maxY;
+        }
+    }
+}
+
+static float calculateCommonVector(float a, float b) {
+    if (a > 0 && b > 0) {
+        return a < b ? a : b;
+    } else if (a < 0 && b < 0) {
+        return a > b ? a : b;
+    } else {
+        return 0;
+    }
+}
+
 
 // --- InputReader ---
 
@@ -1553,10 +1597,32 @@
 
         motionEventEdgeFlags = AMOTION_EVENT_EDGE_FLAG_NONE;
 
+        if (mHaveVWheel && (fields & Accumulator::FIELD_REL_WHEEL)) {
+            vscroll = mAccumulator.relWheel;
+        } else {
+            vscroll = 0;
+        }
+        if (mHaveHWheel && (fields & Accumulator::FIELD_REL_HWHEEL)) {
+            hscroll = mAccumulator.relHWheel;
+        } else {
+            hscroll = 0;
+        }
+
         if (mPointerController != NULL) {
-            mPointerController->move(deltaX, deltaY);
-            if (buttonsChanged) {
-                mPointerController->setButtonState(mLocked.buttonState);
+            if (deltaX != 0 || deltaY != 0 || vscroll != 0 || hscroll != 0
+                    || buttonsChanged) {
+                mPointerController->setPresentation(
+                        PointerControllerInterface::PRESENTATION_POINTER);
+
+                if (deltaX != 0 || deltaY != 0) {
+                    mPointerController->move(deltaX, deltaY);
+                }
+
+                if (buttonsChanged) {
+                    mPointerController->setButtonState(mLocked.buttonState);
+                }
+
+                mPointerController->unfade();
             }
 
             float x, y;
@@ -1574,20 +1640,6 @@
         }
 
         pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, down ? 1.0f : 0.0f);
-
-        if (mHaveVWheel && (fields & Accumulator::FIELD_REL_WHEEL)) {
-            vscroll = mAccumulator.relWheel;
-        } else {
-            vscroll = 0;
-        }
-        if (mHaveHWheel && (fields & Accumulator::FIELD_REL_HWHEEL)) {
-            hscroll = mAccumulator.relHWheel;
-        } else {
-            hscroll = 0;
-        }
-        if (hscroll != 0 || vscroll != 0) {
-            mPointerController->unfade();
-        }
     } // release lock
 
     // Moving an external trackball or mouse should wake the device.
@@ -1751,8 +1803,8 @@
                     mLocked.pointerGestureXZoomScale);
             dump.appendFormat(INDENT4 "YZoomScale: %0.3f\n",
                     mLocked.pointerGestureYZoomScale);
-            dump.appendFormat(INDENT4 "MaxSwipeWidthSquared: %d\n",
-                    mLocked.pointerGestureMaxSwipeWidthSquared);
+            dump.appendFormat(INDENT4 "MaxSwipeWidth: %f\n",
+                    mLocked.pointerGestureMaxSwipeWidth);
         }
     } // release lock
 }
@@ -1825,6 +1877,10 @@
     mParameters.useJumpyTouchFilter = getPolicy()->filterJumpyTouchEvents();
     mParameters.virtualKeyQuietTime = getPolicy()->getVirtualKeyQuietTime();
 
+    // TODO: Make this configurable.
+    //mParameters.gestureMode = Parameters::GESTURE_MODE_POINTER;
+    mParameters.gestureMode = Parameters::GESTURE_MODE_SPOTS;
+
     if (getEventHub()->hasRelativeAxis(getDeviceId(), REL_X)
             || getEventHub()->hasRelativeAxis(getDeviceId(), REL_Y)) {
         // The device is a cursor device with a touch pad attached.
@@ -1983,7 +2039,7 @@
         mLocked.geometricScale = avg(mLocked.xScale, mLocked.yScale);
 
         // Size of diagonal axis.
-        float diagonalSize = pythag(width, height);
+        float diagonalSize = hypotf(width, height);
 
         // TouchMajor and TouchMinor factors.
         if (mCalibration.touchSizeCalibration != Calibration::TOUCH_SIZE_CALIBRATION_NONE) {
@@ -2178,30 +2234,39 @@
         if (mParameters.deviceType == Parameters::DEVICE_TYPE_POINTER) {
             int32_t rawWidth = mRawAxes.x.maxValue - mRawAxes.x.minValue + 1;
             int32_t rawHeight = mRawAxes.y.maxValue - mRawAxes.y.minValue + 1;
+            float rawDiagonal = hypotf(rawWidth, rawHeight);
+            float displayDiagonal = hypotf(mLocked.associatedDisplayWidth,
+                    mLocked.associatedDisplayHeight);
 
-            // Scale movements such that one whole swipe of the touch pad covers a portion
-            // of the display along whichever axis of the touch pad is longer.
+            // Scale movements such that one whole swipe of the touch pad covers a
+            // given area relative to the diagonal size of the display.
             // Assume that the touch pad has a square aspect ratio such that movements in
             // X and Y of the same number of raw units cover the same physical distance.
             const float scaleFactor = 0.8f;
 
-            mLocked.pointerGestureXMovementScale = rawWidth > rawHeight
-                    ? scaleFactor * float(mLocked.associatedDisplayWidth) / rawWidth
-                    : scaleFactor * float(mLocked.associatedDisplayHeight) / rawHeight;
+            mLocked.pointerGestureXMovementScale = GESTURE_MOVEMENT_SPEED_RATIO
+                    * displayDiagonal / rawDiagonal;
             mLocked.pointerGestureYMovementScale = mLocked.pointerGestureXMovementScale;
 
             // Scale zooms to cover a smaller range of the display than movements do.
             // This value determines the area around the pointer that is affected by freeform
             // pointer gestures.
-            mLocked.pointerGestureXZoomScale = mLocked.pointerGestureXMovementScale * 0.4f;
-            mLocked.pointerGestureYZoomScale = mLocked.pointerGestureYMovementScale * 0.4f;
+            mLocked.pointerGestureXZoomScale = GESTURE_ZOOM_SPEED_RATIO
+                    * displayDiagonal / rawDiagonal;
+            mLocked.pointerGestureYZoomScale = mLocked.pointerGestureXZoomScale;
 
-            // Max width between pointers to detect a swipe gesture is 3/4 of the short
-            // axis of the touch pad.  Touches that are wider than this are translated
-            // into freeform gestures.
-            mLocked.pointerGestureMaxSwipeWidthSquared = min(rawWidth, rawHeight) * 3 / 4;
-            mLocked.pointerGestureMaxSwipeWidthSquared *=
-                    mLocked.pointerGestureMaxSwipeWidthSquared;
+            // Max width between pointers to detect a swipe gesture is more than some fraction
+            // of the diagonal axis of the touch pad.  Touches that are wider than this are
+            // translated into freeform gestures.
+            mLocked.pointerGestureMaxSwipeWidth = SWIPE_MAX_WIDTH_RATIO * rawDiagonal;
+
+            // Reset the current pointer gesture.
+            mPointerGesture.reset();
+
+            // Remove any current spots.
+            if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+                mPointerController->clearSpots();
+            }
         }
     }
 
@@ -2628,6 +2693,11 @@
     { // acquire lock
         AutoMutex _l(mLock);
         initializeLocked();
+
+        if (mPointerController != NULL
+                && mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+            mPointerController->clearSpots();
+        }
     } // release lock
 
     InputMapper::reset();
@@ -3070,7 +3140,7 @@
             int32_t c2 = signExtendNybble(in.orientation & 0x0f);
             if (c1 != 0 || c2 != 0) {
                 orientation = atan2f(c1, c2) * 0.5f;
-                float scale = 1.0f + pythag(c1, c2) / 16.0f;
+                float scale = 1.0f + hypotf(c1, c2) / 16.0f;
                 touchMajor *= scale;
                 touchMinor /= scale;
                 toolMajor *= scale;
@@ -3155,22 +3225,35 @@
 }
 
 void TouchInputMapper::dispatchPointerGestures(nsecs_t when, uint32_t policyFlags) {
+    // Switch pointer presentation.
+    mPointerController->setPresentation(
+            mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS
+                    ? PointerControllerInterface::PRESENTATION_SPOT
+                    : PointerControllerInterface::PRESENTATION_POINTER);
+
     // Update current gesture coordinates.
     bool cancelPreviousGesture, finishPreviousGesture;
     preparePointerGestures(when, &cancelPreviousGesture, &finishPreviousGesture);
 
+    // Show the pointer if needed.
+    if (mPointerGesture.currentGestureMode != PointerGesture::NEUTRAL
+            && mPointerGesture.currentGestureMode != PointerGesture::QUIET) {
+        mPointerController->unfade();
+    }
+
     // Send events!
     uint32_t metaState = getContext()->getGlobalMetaState();
 
     // Update last coordinates of pointers that have moved so that we observe the new
     // pointer positions at the same time as other pointers that have just gone up.
     bool down = mPointerGesture.currentGestureMode == PointerGesture::CLICK_OR_DRAG
+            || mPointerGesture.currentGestureMode == PointerGesture::PRESS
             || mPointerGesture.currentGestureMode == PointerGesture::SWIPE
             || mPointerGesture.currentGestureMode == PointerGesture::FREEFORM;
     bool moveNeeded = false;
     if (down && !cancelPreviousGesture && !finishPreviousGesture
-            && mPointerGesture.lastGesturePointerCount != 0
-            && mPointerGesture.currentGesturePointerCount != 0) {
+            && !mPointerGesture.lastGestureIdBits.isEmpty()
+            && !mPointerGesture.currentGestureIdBits.isEmpty()) {
         BitSet32 movedGestureIdBits(mPointerGesture.currentGestureIdBits.value
                 & mPointerGesture.lastGestureIdBits.value);
         moveNeeded = updateMovedPointerCoords(
@@ -3284,11 +3367,8 @@
     // Update state.
     mPointerGesture.lastGestureMode = mPointerGesture.currentGestureMode;
     if (!down) {
-        mPointerGesture.lastGesturePointerCount = 0;
         mPointerGesture.lastGestureIdBits.clear();
     } else {
-        uint32_t currentGesturePointerCount = mPointerGesture.currentGesturePointerCount;
-        mPointerGesture.lastGesturePointerCount = currentGesturePointerCount;
         mPointerGesture.lastGestureIdBits = mPointerGesture.currentGestureIdBits;
         for (BitSet32 idBits(mPointerGesture.currentGestureIdBits); !idBits.isEmpty(); ) {
             uint32_t id = idBits.firstMarkedBit();
@@ -3328,77 +3408,51 @@
     // Choose an arbitrary pointer that just went down, if there is one.
     // Otherwise choose an arbitrary remaining pointer.
     // This guarantees we always have an active touch id when there is at least one pointer.
-    // We always switch to the newest pointer down because that's usually where the user's
-    // attention is focused.
-    int32_t activeTouchId;
-    BitSet32 downTouchIdBits(mCurrentTouch.idBits.value & ~mLastTouch.idBits.value);
-    if (!downTouchIdBits.isEmpty()) {
-        activeTouchId = mPointerGesture.activeTouchId = downTouchIdBits.firstMarkedBit();
-    } else {
-        activeTouchId = mPointerGesture.activeTouchId;
-        if (activeTouchId < 0 || !mCurrentTouch.idBits.hasBit(activeTouchId)) {
-            if (!mCurrentTouch.idBits.isEmpty()) {
-                activeTouchId = mPointerGesture.activeTouchId =
-                        mCurrentTouch.idBits.firstMarkedBit();
-            } else {
-                activeTouchId = mPointerGesture.activeTouchId = -1;
-            }
+    // We keep the same active touch id for as long as possible.
+    bool activeTouchChanged = false;
+    int32_t lastActiveTouchId = mPointerGesture.activeTouchId;
+    int32_t activeTouchId = lastActiveTouchId;
+    if (activeTouchId < 0) {
+        if (!mCurrentTouch.idBits.isEmpty()) {
+            activeTouchChanged = true;
+            activeTouchId = mPointerGesture.activeTouchId = mCurrentTouch.idBits.firstMarkedBit();
+            mPointerGesture.firstTouchTime = when;
         }
-    }
-
-    // Update the touch origin data to track where each finger originally went down.
-    if (mCurrentTouch.pointerCount == 0 || mPointerGesture.touchOrigin.pointerCount == 0) {
-        // Fast path when all fingers have gone up or down.
-        mPointerGesture.touchOrigin.copyFrom(mCurrentTouch);
-    } else {
-        // Slow path when only some fingers have gone up or down.
-        for (BitSet32 idBits(mPointerGesture.touchOrigin.idBits.value
-                & ~mCurrentTouch.idBits.value); !idBits.isEmpty(); ) {
-            uint32_t id = idBits.firstMarkedBit();
-            idBits.clearBit(id);
-            mPointerGesture.touchOrigin.idBits.clearBit(id);
-            uint32_t index = mPointerGesture.touchOrigin.idToIndex[id];
-            uint32_t count = --mPointerGesture.touchOrigin.pointerCount;
-            while (index < count) {
-                mPointerGesture.touchOrigin.pointers[index] =
-                        mPointerGesture.touchOrigin.pointers[index + 1];
-                uint32_t movedId = mPointerGesture.touchOrigin.pointers[index].id;
-                mPointerGesture.touchOrigin.idToIndex[movedId] = index;
-                index += 1;
-            }
-        }
-        for (BitSet32 idBits(mCurrentTouch.idBits.value
-                & ~mPointerGesture.touchOrigin.idBits.value); !idBits.isEmpty(); ) {
-            uint32_t id = idBits.firstMarkedBit();
-            idBits.clearBit(id);
-            mPointerGesture.touchOrigin.idBits.markBit(id);
-            uint32_t index = mPointerGesture.touchOrigin.pointerCount++;
-            mPointerGesture.touchOrigin.pointers[index] =
-                    mCurrentTouch.pointers[mCurrentTouch.idToIndex[id]];
-            mPointerGesture.touchOrigin.idToIndex[id] = index;
+    } else if (!mCurrentTouch.idBits.hasBit(activeTouchId)) {
+        activeTouchChanged = true;
+        if (!mCurrentTouch.idBits.isEmpty()) {
+            activeTouchId = mPointerGesture.activeTouchId = mCurrentTouch.idBits.firstMarkedBit();
+        } else {
+            activeTouchId = mPointerGesture.activeTouchId = -1;
         }
     }
 
     // Determine whether we are in quiet time.
-    bool isQuietTime = when < mPointerGesture.quietTime + QUIET_INTERVAL;
-    if (!isQuietTime) {
-        if ((mPointerGesture.lastGestureMode == PointerGesture::SWIPE
-                || mPointerGesture.lastGestureMode == PointerGesture::FREEFORM)
-                && mCurrentTouch.pointerCount < 2) {
-            // Enter quiet time when exiting swipe or freeform state.
-            // This is to prevent accidentally entering the hover state and flinging the
-            // pointer when finishing a swipe and there is still one pointer left onscreen.
-            isQuietTime = true;
-        } else if (mPointerGesture.lastGestureMode == PointerGesture::CLICK_OR_DRAG
-                && mCurrentTouch.pointerCount >= 2
-                && !isPointerDown(mCurrentTouch.buttonState)) {
-            // Enter quiet time when releasing the button and there are still two or more
-            // fingers down.  This may indicate that one finger was used to press the button
-            // but it has not gone up yet.
-            isQuietTime = true;
-        }
-        if (isQuietTime) {
-            mPointerGesture.quietTime = when;
+    bool isQuietTime = false;
+    if (activeTouchId < 0) {
+        mPointerGesture.resetQuietTime();
+    } else {
+        isQuietTime = when < mPointerGesture.quietTime + QUIET_INTERVAL;
+        if (!isQuietTime) {
+            if ((mPointerGesture.lastGestureMode == PointerGesture::PRESS
+                    || mPointerGesture.lastGestureMode == PointerGesture::SWIPE
+                    || mPointerGesture.lastGestureMode == PointerGesture::FREEFORM)
+                    && mCurrentTouch.pointerCount < 2) {
+                // Enter quiet time when exiting swipe or freeform state.
+                // This is to prevent accidentally entering the hover state and flinging the
+                // pointer when finishing a swipe and there is still one pointer left onscreen.
+                isQuietTime = true;
+            } else if (mPointerGesture.lastGestureMode == PointerGesture::CLICK_OR_DRAG
+                    && mCurrentTouch.pointerCount >= 2
+                    && !isPointerDown(mCurrentTouch.buttonState)) {
+                // Enter quiet time when releasing the button and there are still two or more
+                // fingers down.  This may indicate that one finger was used to press the button
+                // but it has not gone up yet.
+                isQuietTime = true;
+            }
+            if (isQuietTime) {
+                mPointerGesture.quietTime = when;
+            }
         }
     }
 
@@ -3413,10 +3467,17 @@
 
         mPointerGesture.activeGestureId = -1;
         mPointerGesture.currentGestureMode = PointerGesture::QUIET;
-        mPointerGesture.currentGesturePointerCount = 0;
         mPointerGesture.currentGestureIdBits.clear();
+
+        mPointerController->setButtonState(0);
+
+        if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+            mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_NEUTRAL;
+            mPointerGesture.spotIdBits.clear();
+            moveSpotsLocked();
+        }
     } else if (isPointerDown(mCurrentTouch.buttonState)) {
-        // Case 2: Button is pressed. (DRAG)
+        // Case 2: Button is pressed. (CLICK_OR_DRAG)
         // The pointer follows the active touch point.
         // Emit DOWN, MOVE, UP events at the pointer location.
         //
@@ -3449,7 +3510,7 @@
                     uint32_t id = mCurrentTouch.pointers[i].id;
                     float vx, vy;
                     if (mPointerGesture.velocityTracker.getVelocity(id, &vx, &vy)) {
-                        float speed = pythag(vx, vy);
+                        float speed = hypotf(vx, vy);
                         if (speed > bestSpeed) {
                             bestId = id;
                             bestSpeed = speed;
@@ -3458,6 +3519,7 @@
                 }
                 if (bestId >= 0 && bestId != activeTouchId) {
                     mPointerGesture.activeTouchId = activeTouchId = bestId;
+                    activeTouchChanged = true;
 #if DEBUG_GESTURES
                     LOGD("Gestures: CLICK_OR_DRAG switched pointers, "
                             "bestId=%d, bestSpeed=%0.3f", bestId, bestSpeed);
@@ -3474,6 +3536,10 @@
                         * mLocked.pointerGestureXMovementScale;
                 float deltaY = (currentPointer.y - lastPointer.y)
                         * mLocked.pointerGestureYMovementScale;
+
+                // Move the pointer using a relative motion.
+                // When using spots, the click will occur at the position of the anchor
+                // spot and all other spots will move there.
                 mPointerController->move(deltaX, deltaY);
             }
         }
@@ -3482,7 +3548,6 @@
         mPointerController->getPosition(&x, &y);
 
         mPointerGesture.currentGestureMode = PointerGesture::CLICK_OR_DRAG;
-        mPointerGesture.currentGesturePointerCount = 1;
         mPointerGesture.currentGestureIdBits.clear();
         mPointerGesture.currentGestureIdBits.markBit(mPointerGesture.activeGestureId);
         mPointerGesture.currentGestureIdToIndex[mPointerGesture.activeGestureId] = 0;
@@ -3490,26 +3555,49 @@
         mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_X, x);
         mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_Y, y);
         mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, 1.0f);
+
+        mPointerController->setButtonState(BUTTON_STATE_PRIMARY);
+
+        if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+            if (activeTouchId >= 0) {
+                // Collapse all spots into one point at the pointer location.
+                mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_BUTTON_DRAG;
+                mPointerGesture.spotIdBits.clear();
+                for (uint32_t i = 0; i < mCurrentTouch.pointerCount; i++) {
+                    uint32_t id = mCurrentTouch.pointers[i].id;
+                    mPointerGesture.spotIdBits.markBit(id);
+                    mPointerGesture.spotIdToIndex[id] = i;
+                    mPointerGesture.spotCoords[i] = mPointerGesture.currentGestureCoords[0];
+                }
+            } else {
+                // No fingers.  Generate a spot at the pointer location so the
+                // anchor appears to be pressed.
+                mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_BUTTON_CLICK;
+                mPointerGesture.spotIdBits.clear();
+                mPointerGesture.spotIdBits.markBit(0);
+                mPointerGesture.spotIdToIndex[0] = 0;
+                mPointerGesture.spotCoords[0] = mPointerGesture.currentGestureCoords[0];
+            }
+            moveSpotsLocked();
+        }
     } else if (mCurrentTouch.pointerCount == 0) {
         // Case 3. No fingers down and button is not pressed. (NEUTRAL)
         *outFinishPreviousGesture = true;
 
-        // Watch for taps coming out of HOVER or INDETERMINATE_MULTITOUCH mode.
+        // Watch for taps coming out of HOVER mode.
         bool tapped = false;
         if (mPointerGesture.lastGestureMode == PointerGesture::HOVER
-                || mPointerGesture.lastGestureMode
-                        == PointerGesture::INDETERMINATE_MULTITOUCH) {
+                && mLastTouch.pointerCount == 1) {
             if (when <= mPointerGesture.tapTime + TAP_INTERVAL) {
                 float x, y;
                 mPointerController->getPosition(&x, &y);
-                if (fabs(x - mPointerGesture.initialPointerX) <= TAP_SLOP
-                        && fabs(y - mPointerGesture.initialPointerY) <= TAP_SLOP) {
+                if (fabs(x - mPointerGesture.tapX) <= TAP_SLOP
+                        && fabs(y - mPointerGesture.tapY) <= TAP_SLOP) {
 #if DEBUG_GESTURES
                     LOGD("Gestures: TAP");
 #endif
                     mPointerGesture.activeGestureId = 0;
                     mPointerGesture.currentGestureMode = PointerGesture::TAP;
-                    mPointerGesture.currentGesturePointerCount = 1;
                     mPointerGesture.currentGestureIdBits.clear();
                     mPointerGesture.currentGestureIdBits.markBit(
                             mPointerGesture.activeGestureId);
@@ -3517,17 +3605,30 @@
                             mPointerGesture.activeGestureId] = 0;
                     mPointerGesture.currentGestureCoords[0].clear();
                     mPointerGesture.currentGestureCoords[0].setAxisValue(
-                            AMOTION_EVENT_AXIS_X, mPointerGesture.initialPointerX);
+                            AMOTION_EVENT_AXIS_X, mPointerGesture.tapX);
                     mPointerGesture.currentGestureCoords[0].setAxisValue(
-                            AMOTION_EVENT_AXIS_Y, mPointerGesture.initialPointerY);
+                            AMOTION_EVENT_AXIS_Y, mPointerGesture.tapY);
                     mPointerGesture.currentGestureCoords[0].setAxisValue(
                             AMOTION_EVENT_AXIS_PRESSURE, 1.0f);
+
+                    mPointerController->setButtonState(BUTTON_STATE_PRIMARY);
+                    mPointerController->setButtonState(0);
+
+                    if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+                        mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_TAP;
+                        mPointerGesture.spotIdBits.clear();
+                        mPointerGesture.spotIdBits.markBit(lastActiveTouchId);
+                        mPointerGesture.spotIdToIndex[lastActiveTouchId] = 0;
+                        mPointerGesture.spotCoords[0] = mPointerGesture.currentGestureCoords[0];
+                        moveSpotsLocked();
+                    }
+
                     tapped = true;
                 } else {
 #if DEBUG_GESTURES
                     LOGD("Gestures: Not a TAP, deltaX=%f, deltaY=%f",
-                            x - mPointerGesture.initialPointerX,
-                            y - mPointerGesture.initialPointerY);
+                            x - mPointerGesture.tapX,
+                            y - mPointerGesture.tapY);
 #endif
                 }
             } else {
@@ -3537,14 +3638,22 @@
 #endif
             }
         }
+
         if (!tapped) {
 #if DEBUG_GESTURES
             LOGD("Gestures: NEUTRAL");
 #endif
             mPointerGesture.activeGestureId = -1;
             mPointerGesture.currentGestureMode = PointerGesture::NEUTRAL;
-            mPointerGesture.currentGesturePointerCount = 0;
             mPointerGesture.currentGestureIdBits.clear();
+
+            mPointerController->setButtonState(0);
+
+            if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+                mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_NEUTRAL;
+                mPointerGesture.spotIdBits.clear();
+                moveSpotsLocked();
+            }
         }
     } else if (mCurrentTouch.pointerCount == 1) {
         // Case 4. Exactly one finger down, button is not pressed. (HOVER)
@@ -3565,6 +3674,9 @@
                     * mLocked.pointerGestureXMovementScale;
             float deltaY = (currentPointer.y - lastPointer.y)
                     * mLocked.pointerGestureYMovementScale;
+
+            // Move the pointer using a relative motion.
+            // When using spots, the hover will occur at the position of the anchor spot.
             mPointerController->move(deltaX, deltaY);
         }
 
@@ -3575,7 +3687,6 @@
         mPointerController->getPosition(&x, &y);
 
         mPointerGesture.currentGestureMode = PointerGesture::HOVER;
-        mPointerGesture.currentGesturePointerCount = 1;
         mPointerGesture.currentGestureIdBits.clear();
         mPointerGesture.currentGestureIdBits.markBit(mPointerGesture.activeGestureId);
         mPointerGesture.currentGestureIdToIndex[mPointerGesture.activeGestureId] = 0;
@@ -3586,159 +3697,268 @@
 
         if (mLastTouch.pointerCount == 0 && mCurrentTouch.pointerCount != 0) {
             mPointerGesture.tapTime = when;
-            mPointerGesture.initialPointerX = x;
-            mPointerGesture.initialPointerY = y;
+            mPointerGesture.tapX = x;
+            mPointerGesture.tapY = y;
+        }
+
+        mPointerController->setButtonState(0);
+
+        if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+            mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_HOVER;
+            mPointerGesture.spotIdBits.clear();
+            mPointerGesture.spotIdBits.markBit(activeTouchId);
+            mPointerGesture.spotIdToIndex[activeTouchId] = 0;
+            mPointerGesture.spotCoords[0] = mPointerGesture.currentGestureCoords[0];
+            moveSpotsLocked();
         }
     } else {
-        // Case 5. At least two fingers down, button is not pressed. (SWIPE or FREEFORM
-        // or INDETERMINATE_MULTITOUCH)
-        // Initially we watch and wait for something interesting to happen so as to
-        // avoid making a spurious guess as to the nature of the gesture.  For example,
-        // the fingers may be in transition to some other state such as pressing or
-        // releasing the button or we may be performing a two finger tap.
+        // Case 5. At least two fingers down, button is not pressed. (PRESS, SWIPE or FREEFORM)
+        // We need to provide feedback for each finger that goes down so we cannot wait
+        // for the fingers to move before deciding what to do.
         //
-        // Fix the centroid of the figure when the gesture actually starts.
-        // We do not recalculate the centroid at any other time during the gesture because
-        // it would affect the relationship of the touch points relative to the pointer location.
+        // The ambiguous case is deciding what to do when there are two fingers down but they
+        // have not moved enough to determine whether they are part of a drag or part of a
+        // freeform gesture, or just a press or long-press at the pointer location.
+        //
+        // When there are two fingers we start with the PRESS hypothesis and we generate a
+        // down at the pointer location.
+        //
+        // When the two fingers move enough or when additional fingers are added, we make
+        // a decision to transition into SWIPE or FREEFORM mode accordingly.
         LOG_ASSERT(activeTouchId >= 0);
 
-        uint32_t currentTouchPointerCount = mCurrentTouch.pointerCount;
-        if (currentTouchPointerCount > MAX_POINTERS) {
-            currentTouchPointerCount = MAX_POINTERS;
-        }
-
-        if (mPointerGesture.lastGestureMode != PointerGesture::INDETERMINATE_MULTITOUCH
+        bool needReference = false;
+        bool settled = when >= mPointerGesture.firstTouchTime + MULTITOUCH_SETTLE_INTERVAL;
+        if (mPointerGesture.lastGestureMode != PointerGesture::PRESS
                 && mPointerGesture.lastGestureMode != PointerGesture::SWIPE
                 && mPointerGesture.lastGestureMode != PointerGesture::FREEFORM) {
-            mPointerGesture.currentGestureMode = PointerGesture::INDETERMINATE_MULTITOUCH;
-
             *outFinishPreviousGesture = true;
-            mPointerGesture.activeGestureId = -1;
+            mPointerGesture.currentGestureMode = PointerGesture::PRESS;
+            mPointerGesture.activeGestureId = 0;
 
-            // Remember the initial pointer location.
-            // Everything we do will be relative to this location.
-            mPointerController->getPosition(&mPointerGesture.initialPointerX,
-                    &mPointerGesture.initialPointerY);
-
-            // Track taps.
-            if (mLastTouch.pointerCount == 0 && mCurrentTouch.pointerCount != 0) {
-                mPointerGesture.tapTime = when;
+            if (settled && mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS
+                    && mLastTouch.idBits.hasBit(mPointerGesture.activeTouchId)) {
+                // The spot is already visible and has settled, use it as the reference point
+                // for the gesture.  Other spots will be positioned relative to this one.
+#if DEBUG_GESTURES
+                LOGD("Gestures: Using active spot as reference for MULTITOUCH, "
+                        "settle time expired %0.3fms ago",
+                        (when - mPointerGesture.firstTouchTime - MULTITOUCH_SETTLE_INTERVAL)
+                                * 0.000001f);
+#endif
+                const PointerData& d = mLastTouch.pointers[mLastTouch.idToIndex[
+                        mPointerGesture.activeTouchId]];
+                mPointerGesture.referenceTouchX = d.x;
+                mPointerGesture.referenceTouchY = d.y;
+                const PointerCoords& c = mPointerGesture.spotCoords[mPointerGesture.spotIdToIndex[
+                        mPointerGesture.activeTouchId]];
+                mPointerGesture.referenceGestureX = c.getAxisValue(AMOTION_EVENT_AXIS_X);
+                mPointerGesture.referenceGestureY = c.getAxisValue(AMOTION_EVENT_AXIS_Y);
+            } else {
+#if DEBUG_GESTURES
+                LOGD("Gestures: Using centroid as reference for MULTITOUCH, "
+                        "settle time remaining %0.3fms",
+                        (mPointerGesture.firstTouchTime + MULTITOUCH_SETTLE_INTERVAL - when)
+                                * 0.000001f);
+#endif
+                needReference = true;
             }
-
-            // Reset the touch origin to be relative to exactly where the fingers are now
-            // in case they have moved some distance away as part of a previous gesture.
-            // We want to know how far the fingers have traveled since we started considering
-            // a multitouch gesture.
-            mPointerGesture.touchOrigin.copyFrom(mCurrentTouch);
+        } else if (!settled && mCurrentTouch.pointerCount > mLastTouch.pointerCount) {
+            // Additional pointers have gone down but not yet settled.
+            // Reset the gesture.
+#if DEBUG_GESTURES
+            LOGD("Gestures: Resetting gesture since additional pointers went down for MULTITOUCH, "
+                    "settle time remaining %0.3fms",
+                    (mPointerGesture.firstTouchTime + MULTITOUCH_SETTLE_INTERVAL - when)
+                            * 0.000001f);
+#endif
+            *outCancelPreviousGesture = true;
+            mPointerGesture.currentGestureMode = PointerGesture::PRESS;
+            mPointerGesture.activeGestureId = 0;
         } else {
+            // Continue previous gesture.
             mPointerGesture.currentGestureMode = mPointerGesture.lastGestureMode;
         }
 
-        if (mPointerGesture.currentGestureMode == PointerGesture::INDETERMINATE_MULTITOUCH) {
-            // Wait for the pointers to start moving before doing anything.
-            bool decideNow = true;
-            for (uint32_t i = 0; i < currentTouchPointerCount; i++) {
-                const PointerData& current = mCurrentTouch.pointers[i];
-                const PointerData& origin = mPointerGesture.touchOrigin.pointers[
-                        mPointerGesture.touchOrigin.idToIndex[current.id]];
-                float distance = pythag(
-                        (current.x - origin.x) * mLocked.pointerGestureXZoomScale,
-                        (current.y - origin.y) * mLocked.pointerGestureYZoomScale);
-                if (distance < MULTITOUCH_MIN_TRAVEL) {
-                    decideNow = false;
-                    break;
-                }
-            }
+        if (needReference) {
+            // Use the centroid and pointer location as the reference points for the gesture.
+            mCurrentTouch.getCentroid(&mPointerGesture.referenceTouchX,
+                    &mPointerGesture.referenceTouchY);
+            mPointerController->getPosition(&mPointerGesture.referenceGestureX,
+                    &mPointerGesture.referenceGestureY);
+        }
 
-            if (decideNow) {
+        if (mPointerGesture.currentGestureMode == PointerGesture::PRESS) {
+            float d;
+            if (mCurrentTouch.pointerCount > 2) {
+                // There are more than two pointers, switch to FREEFORM.
+#if DEBUG_GESTURES
+                LOGD("Gestures: PRESS transitioned to FREEFORM, number of pointers %d > 2",
+                        mCurrentTouch.pointerCount);
+#endif
+                *outCancelPreviousGesture = true;
                 mPointerGesture.currentGestureMode = PointerGesture::FREEFORM;
-                if (currentTouchPointerCount == 2
-                        && distanceSquared(
-                                mCurrentTouch.pointers[0].x, mCurrentTouch.pointers[0].y,
-                                mCurrentTouch.pointers[1].x, mCurrentTouch.pointers[1].y)
-                                <= mLocked.pointerGestureMaxSwipeWidthSquared) {
-                    const PointerData& current1 = mCurrentTouch.pointers[0];
-                    const PointerData& current2 = mCurrentTouch.pointers[1];
-                    const PointerData& origin1 = mPointerGesture.touchOrigin.pointers[
-                            mPointerGesture.touchOrigin.idToIndex[current1.id]];
-                    const PointerData& origin2 = mPointerGesture.touchOrigin.pointers[
-                            mPointerGesture.touchOrigin.idToIndex[current2.id]];
+            } else if (((d = distance(
+                    mCurrentTouch.pointers[0].x, mCurrentTouch.pointers[0].y,
+                    mCurrentTouch.pointers[1].x, mCurrentTouch.pointers[1].y))
+                            > mLocked.pointerGestureMaxSwipeWidth)) {
+                // There are two pointers but they are too far apart, switch to FREEFORM.
+#if DEBUG_GESTURES
+                LOGD("Gestures: PRESS transitioned to FREEFORM, distance %0.3f > %0.3f",
+                        d, mLocked.pointerGestureMaxSwipeWidth);
+#endif
+                *outCancelPreviousGesture = true;
+                mPointerGesture.currentGestureMode = PointerGesture::FREEFORM;
+            } else {
+                // There are two pointers.  Wait for both pointers to start moving
+                // before deciding whether this is a SWIPE or FREEFORM gesture.
+                uint32_t id1 = mCurrentTouch.pointers[0].id;
+                uint32_t id2 = mCurrentTouch.pointers[1].id;
 
-                    float x1 = (current1.x - origin1.x) * mLocked.pointerGestureXZoomScale;
-                    float y1 = (current1.y - origin1.y) * mLocked.pointerGestureYZoomScale;
-                    float x2 = (current2.x - origin2.x) * mLocked.pointerGestureXZoomScale;
-                    float y2 = (current2.y - origin2.y) * mLocked.pointerGestureYZoomScale;
-                    float magnitude1 = pythag(x1, y1);
-                    float magnitude2 = pythag(x2, y2);
+                float vx1, vy1, vx2, vy2;
+                mPointerGesture.velocityTracker.getVelocity(id1, &vx1, &vy1);
+                mPointerGesture.velocityTracker.getVelocity(id2, &vx2, &vy2);
 
-                    // Calculate the dot product of the vectors.
+                float speed1 = hypotf(vx1, vy1);
+                float speed2 = hypotf(vx2, vy2);
+                if (speed1 >= MULTITOUCH_MIN_SPEED && speed2 >= MULTITOUCH_MIN_SPEED) {
+                    // Calculate the dot product of the velocity vectors.
                     // When the vectors are oriented in approximately the same direction,
                     // the angle betweeen them is near zero and the cosine of the angle
                     // approches 1.0.  Recall that dot(v1, v2) = cos(angle) * mag(v1) * mag(v2).
-                    // We know that the magnitude is at least MULTITOUCH_MIN_TRAVEL because
-                    // we checked it above.
-                    float dot = x1 * x2 + y1 * y2;
-                    float cosine = dot / (magnitude1 * magnitude2); // denominator always > 0
-                    if (cosine > SWIPE_TRANSITION_ANGLE_COSINE) {
+                    float dot = vx1 * vx2 + vy1 * vy2;
+                    float cosine = dot / (speed1 * speed2); // denominator always > 0
+                    if (cosine >= SWIPE_TRANSITION_ANGLE_COSINE) {
+                        // Pointers are moving in the same direction.  Switch to SWIPE.
+#if DEBUG_GESTURES
+                        LOGD("Gestures: PRESS transitioned to SWIPE, "
+                                "speed1 %0.3f >= %0.3f, speed2 %0.3f >= %0.3f, "
+                                "cosine %0.3f >= %0.3f",
+                                speed1, MULTITOUCH_MIN_SPEED, speed2, MULTITOUCH_MIN_SPEED,
+                                cosine, SWIPE_TRANSITION_ANGLE_COSINE);
+#endif
                         mPointerGesture.currentGestureMode = PointerGesture::SWIPE;
+                    } else {
+                        // Pointers are moving in different directions.  Switch to FREEFORM.
+#if DEBUG_GESTURES
+                        LOGD("Gestures: PRESS transitioned to FREEFORM, "
+                                "speed1 %0.3f >= %0.3f, speed2 %0.3f >= %0.3f, "
+                                "cosine %0.3f < %0.3f",
+                                speed1, MULTITOUCH_MIN_SPEED, speed2, MULTITOUCH_MIN_SPEED,
+                                cosine, SWIPE_TRANSITION_ANGLE_COSINE);
+#endif
+                        *outCancelPreviousGesture = true;
+                        mPointerGesture.currentGestureMode = PointerGesture::FREEFORM;
                     }
                 }
-
-                // Remember the initial centroid for the duration of the gesture.
-                mPointerGesture.initialCentroidX = 0;
-                mPointerGesture.initialCentroidY = 0;
-                for (uint32_t i = 0; i < currentTouchPointerCount; i++) {
-                    const PointerData& touch = mCurrentTouch.pointers[i];
-                    mPointerGesture.initialCentroidX += touch.x;
-                    mPointerGesture.initialCentroidY += touch.y;
-                }
-                mPointerGesture.initialCentroidX /= int32_t(currentTouchPointerCount);
-                mPointerGesture.initialCentroidY /= int32_t(currentTouchPointerCount);
-
-                mPointerGesture.activeGestureId = 0;
             }
         } else if (mPointerGesture.currentGestureMode == PointerGesture::SWIPE) {
-            // Switch to FREEFORM if additional pointers go down.
-            if (currentTouchPointerCount > 2) {
+            // Switch from SWIPE to FREEFORM if additional pointers go down.
+            // Cancel previous gesture.
+            if (mCurrentTouch.pointerCount > 2) {
+#if DEBUG_GESTURES
+                LOGD("Gestures: SWIPE transitioned to FREEFORM, number of pointers %d > 2",
+                        mCurrentTouch.pointerCount);
+#endif
                 *outCancelPreviousGesture = true;
                 mPointerGesture.currentGestureMode = PointerGesture::FREEFORM;
             }
         }
 
-        if (mPointerGesture.currentGestureMode == PointerGesture::SWIPE) {
-            // SWIPE mode.
+        // Move the reference points based on the overall group motion of the fingers.
+        // The objective is to calculate a vector delta that is common to the movement
+        // of all fingers.
+        BitSet32 commonIdBits(mLastTouch.idBits.value & mCurrentTouch.idBits.value);
+        if (!commonIdBits.isEmpty()) {
+            float commonDeltaX = 0, commonDeltaY = 0;
+            for (BitSet32 idBits(commonIdBits); !idBits.isEmpty(); ) {
+                bool first = (idBits == commonIdBits);
+                uint32_t id = idBits.firstMarkedBit();
+                idBits.clearBit(id);
+
+                const PointerData& cpd = mCurrentTouch.pointers[mCurrentTouch.idToIndex[id]];
+                const PointerData& lpd = mLastTouch.pointers[mLastTouch.idToIndex[id]];
+                float deltaX = cpd.x - lpd.x;
+                float deltaY = cpd.y - lpd.y;
+
+                if (first) {
+                    commonDeltaX = deltaX;
+                    commonDeltaY = deltaY;
+                } else {
+                    commonDeltaX = calculateCommonVector(commonDeltaX, deltaX);
+                    commonDeltaY = calculateCommonVector(commonDeltaY, deltaY);
+                }
+            }
+
+            mPointerGesture.referenceTouchX += commonDeltaX;
+            mPointerGesture.referenceTouchY += commonDeltaY;
+            mPointerGesture.referenceGestureX +=
+                    commonDeltaX * mLocked.pointerGestureXMovementScale;
+            mPointerGesture.referenceGestureY +=
+                    commonDeltaY * mLocked.pointerGestureYMovementScale;
+            clampPositionUsingPointerBounds(mPointerController,
+                    &mPointerGesture.referenceGestureX,
+                    &mPointerGesture.referenceGestureY);
+        }
+
+        // Report gestures.
+        if (mPointerGesture.currentGestureMode == PointerGesture::PRESS) {
+            // PRESS mode.
 #if DEBUG_GESTURES
-            LOGD("Gestures: SWIPE activeTouchId=%d,"
+            LOGD("Gestures: PRESS activeTouchId=%d,"
                     "activeGestureId=%d, currentTouchPointerCount=%d",
-                    activeTouchId, mPointerGesture.activeGestureId, currentTouchPointerCount);
+                    activeTouchId, mPointerGesture.activeGestureId, mCurrentTouch.pointerCount);
 #endif
             LOG_ASSERT(mPointerGesture.activeGestureId >= 0);
 
-            float x = (mCurrentTouch.pointers[0].x + mCurrentTouch.pointers[1].x
-                    - mPointerGesture.initialCentroidX * 2) * 0.5f
-                    * mLocked.pointerGestureXMovementScale + mPointerGesture.initialPointerX;
-            float y = (mCurrentTouch.pointers[0].y + mCurrentTouch.pointers[1].y
-                    - mPointerGesture.initialCentroidY * 2) * 0.5f
-                    * mLocked.pointerGestureYMovementScale + mPointerGesture.initialPointerY;
-
-            mPointerGesture.currentGesturePointerCount = 1;
             mPointerGesture.currentGestureIdBits.clear();
             mPointerGesture.currentGestureIdBits.markBit(mPointerGesture.activeGestureId);
             mPointerGesture.currentGestureIdToIndex[mPointerGesture.activeGestureId] = 0;
             mPointerGesture.currentGestureCoords[0].clear();
-            mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_X, x);
-            mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_Y, y);
+            mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_X,
+                    mPointerGesture.referenceGestureX);
+            mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_Y,
+                    mPointerGesture.referenceGestureY);
             mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, 1.0f);
+
+            mPointerController->setButtonState(BUTTON_STATE_PRIMARY);
+
+            if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+                mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_PRESS;
+            }
+        } else if (mPointerGesture.currentGestureMode == PointerGesture::SWIPE) {
+            // SWIPE mode.
+#if DEBUG_GESTURES
+            LOGD("Gestures: SWIPE activeTouchId=%d,"
+                    "activeGestureId=%d, currentTouchPointerCount=%d",
+                    activeTouchId, mPointerGesture.activeGestureId, mCurrentTouch.pointerCount);
+#endif
+            LOG_ASSERT(mPointerGesture.activeGestureId >= 0);
+
+            mPointerGesture.currentGestureIdBits.clear();
+            mPointerGesture.currentGestureIdBits.markBit(mPointerGesture.activeGestureId);
+            mPointerGesture.currentGestureIdToIndex[mPointerGesture.activeGestureId] = 0;
+            mPointerGesture.currentGestureCoords[0].clear();
+            mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_X,
+                    mPointerGesture.referenceGestureX);
+            mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_Y,
+                    mPointerGesture.referenceGestureY);
+            mPointerGesture.currentGestureCoords[0].setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, 1.0f);
+
+            mPointerController->setButtonState(0); // touch is not actually following the pointer
+
+            if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+                mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_SWIPE;
+            }
         } else if (mPointerGesture.currentGestureMode == PointerGesture::FREEFORM) {
             // FREEFORM mode.
 #if DEBUG_GESTURES
             LOGD("Gestures: FREEFORM activeTouchId=%d,"
                     "activeGestureId=%d, currentTouchPointerCount=%d",
-                    activeTouchId, mPointerGesture.activeGestureId, currentTouchPointerCount);
+                    activeTouchId, mPointerGesture.activeGestureId, mCurrentTouch.pointerCount);
 #endif
             LOG_ASSERT(mPointerGesture.activeGestureId >= 0);
 
-            mPointerGesture.currentGesturePointerCount = currentTouchPointerCount;
             mPointerGesture.currentGestureIdBits.clear();
 
             BitSet32 mappedTouchIdBits;
@@ -3782,7 +4002,7 @@
                     mPointerGesture.activeGestureId);
 #endif
 
-            for (uint32_t i = 0; i < currentTouchPointerCount; i++) {
+            for (uint32_t i = 0; i < mCurrentTouch.pointerCount; i++) {
                 uint32_t touchId = mCurrentTouch.pointers[i].id;
                 uint32_t gestureId;
                 if (!mappedTouchIdBits.hasBit(touchId)) {
@@ -3805,10 +4025,10 @@
                 mPointerGesture.currentGestureIdBits.markBit(gestureId);
                 mPointerGesture.currentGestureIdToIndex[gestureId] = i;
 
-                float x = (mCurrentTouch.pointers[i].x - mPointerGesture.initialCentroidX)
-                        * mLocked.pointerGestureXZoomScale + mPointerGesture.initialPointerX;
-                float y = (mCurrentTouch.pointers[i].y - mPointerGesture.initialCentroidY)
-                        * mLocked.pointerGestureYZoomScale + mPointerGesture.initialPointerY;
+                float x = (mCurrentTouch.pointers[i].x - mPointerGesture.referenceTouchX)
+                        * mLocked.pointerGestureXZoomScale + mPointerGesture.referenceGestureX;
+                float y = (mCurrentTouch.pointers[i].y - mPointerGesture.referenceTouchY)
+                        * mLocked.pointerGestureYZoomScale + mPointerGesture.referenceGestureY;
 
                 mPointerGesture.currentGestureCoords[i].clear();
                 mPointerGesture.currentGestureCoords[i].setAxisValue(
@@ -3827,30 +4047,45 @@
                         "activeGestureId=%d", mPointerGesture.activeGestureId);
 #endif
             }
-        } else {
-            // INDETERMINATE_MULTITOUCH mode.
-            // Do nothing.
-#if DEBUG_GESTURES
-            LOGD("Gestures: INDETERMINATE_MULTITOUCH");
-#endif
-        }
-    }
 
-    // Unfade the pointer if the user is doing anything with the touch pad.
-    mPointerController->setButtonState(mCurrentTouch.buttonState);
-    if (mCurrentTouch.buttonState || mCurrentTouch.pointerCount != 0) {
-        mPointerController->unfade();
+            mPointerController->setButtonState(0); // touch is not actually following the pointer
+
+            if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+                mPointerGesture.spotGesture = PointerControllerInterface::SPOT_GESTURE_FREEFORM;
+            }
+        }
+
+        // Update spot locations for PRESS, SWIPE and FREEFORM.
+        // We use the same calculation as we do to calculate the gesture pointers
+        // for FREEFORM so that the spots smoothly track gestures.
+        if (mParameters.gestureMode == Parameters::GESTURE_MODE_SPOTS) {
+            mPointerGesture.spotIdBits.clear();
+            for (uint32_t i = 0; i < mCurrentTouch.pointerCount; i++) {
+                uint32_t id = mCurrentTouch.pointers[i].id;
+                mPointerGesture.spotIdBits.markBit(id);
+                mPointerGesture.spotIdToIndex[id] = i;
+
+                float x = (mCurrentTouch.pointers[i].x - mPointerGesture.referenceTouchX)
+                        * mLocked.pointerGestureXZoomScale + mPointerGesture.referenceGestureX;
+                float y = (mCurrentTouch.pointers[i].y - mPointerGesture.referenceTouchY)
+                        * mLocked.pointerGestureYZoomScale + mPointerGesture.referenceGestureY;
+
+                mPointerGesture.spotCoords[i].clear();
+                mPointerGesture.spotCoords[i].setAxisValue(AMOTION_EVENT_AXIS_X, x);
+                mPointerGesture.spotCoords[i].setAxisValue(AMOTION_EVENT_AXIS_Y, y);
+                mPointerGesture.spotCoords[i].setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, 1.0f);
+            }
+            moveSpotsLocked();
+        }
     }
 
 #if DEBUG_GESTURES
     LOGD("Gestures: finishPreviousGesture=%s, cancelPreviousGesture=%s, "
-            "currentGestureMode=%d, currentGesturePointerCount=%d, currentGestureIdBits=0x%08x, "
-            "lastGestureMode=%d, lastGesturePointerCount=%d, lastGestureIdBits=0x%08x",
+            "currentGestureMode=%d, currentGestureIdBits=0x%08x, "
+            "lastGestureMode=%d, lastGestureIdBits=0x%08x",
             toString(*outFinishPreviousGesture), toString(*outCancelPreviousGesture),
-            mPointerGesture.currentGestureMode, mPointerGesture.currentGesturePointerCount,
-            mPointerGesture.currentGestureIdBits.value,
-            mPointerGesture.lastGestureMode, mPointerGesture.lastGesturePointerCount,
-            mPointerGesture.lastGestureIdBits.value);
+            mPointerGesture.currentGestureMode, mPointerGesture.currentGestureIdBits.value,
+            mPointerGesture.lastGestureMode, mPointerGesture.lastGestureIdBits.value);
     for (BitSet32 idBits = mPointerGesture.currentGestureIdBits; !idBits.isEmpty(); ) {
         uint32_t id = idBits.firstMarkedBit();
         idBits.clearBit(id);
@@ -3874,6 +4109,11 @@
 #endif
 }
 
+void TouchInputMapper::moveSpotsLocked() {
+    mPointerController->setSpots(mPointerGesture.spotGesture,
+            mPointerGesture.spotCoords, mPointerGesture.spotIdToIndex, mPointerGesture.spotIdBits);
+}
+
 void TouchInputMapper::dispatchMotion(nsecs_t when, uint32_t policyFlags, uint32_t source,
         int32_t action, int32_t flags, uint32_t metaState, int32_t edgeFlags,
         const PointerCoords* coords, const uint32_t* idToIndex, BitSet32 idBits,
diff --git a/services/input/InputReader.h b/services/input/InputReader.h
index 9ed1391..9b2f4d2 100644
--- a/services/input/InputReader.h
+++ b/services/input/InputReader.h
@@ -20,7 +20,6 @@
 #include "EventHub.h"
 #include "InputDispatcher.h"
 #include "PointerController.h"
-#include "SpotController.h"
 
 #include <ui/Input.h>
 #include <ui/DisplayInfo.h>
@@ -90,9 +89,6 @@
 
     /* Gets a pointer controller associated with the specified cursor device (ie. a mouse). */
     virtual sp<PointerControllerInterface> obtainPointerController(int32_t deviceId) = 0;
-
-    /* Gets a spot controller associated with the specified touch pad device. */
-    virtual sp<SpotControllerInterface> obtainSpotController(int32_t deviceId) = 0;
 };
 
 
@@ -648,6 +644,20 @@
             idBits.clear();
             buttonState = 0;
         }
+
+        void getCentroid(float* outX, float* outY) {
+            float x = 0, y = 0;
+            if (pointerCount != 0) {
+                for (uint32_t i = 0; i < pointerCount; i++) {
+                    x += pointers[i].x;
+                    y += pointers[i].y;
+                }
+                x /= pointerCount;
+                y /= pointerCount;
+            }
+            *outX = x;
+            *outY = y;
+        }
     };
 
     // Input sources supported by the device.
@@ -670,6 +680,12 @@
         bool useJumpyTouchFilter;
         bool useAveragingTouchFilter;
         nsecs_t virtualKeyQuietTime;
+
+        enum GestureMode {
+            GESTURE_MODE_POINTER,
+            GESTURE_MODE_SPOTS,
+        };
+        GestureMode gestureMode;
     } mParameters;
 
     // Immutable calibration parameters in parsed form.
@@ -841,8 +857,8 @@
         float pointerGestureXZoomScale;
         float pointerGestureYZoomScale;
 
-        // The maximum swipe width squared.
-        int32_t pointerGestureMaxSwipeWidthSquared;
+        // The maximum swipe width.
+        float pointerGestureMaxSwipeWidth;
     } mLocked;
 
     virtual void configureParameters();
@@ -929,28 +945,32 @@
             // Emits HOVER_MOVE events at the pointer location.
             HOVER,
 
-            // More than two fingers involved but they haven't moved enough for us
-            // to figure out what is intended.
-            INDETERMINATE_MULTITOUCH,
+            // Exactly two fingers but neither have moved enough to clearly indicate
+            // whether a swipe or freeform gesture was intended.  We consider the
+            // pointer to be pressed so this enables clicking or long-pressing on buttons.
+            // Pointer does not move.
+            // Emits DOWN, MOVE and UP events with a single stationary pointer coordinate.
+            PRESS,
 
             // Exactly two fingers moving in the same direction, button is not pressed.
             // Pointer does not move.
             // Emits DOWN, MOVE and UP events with a single pointer coordinate that
             // follows the midpoint between both fingers.
-            // The centroid is fixed when entering this state.
             SWIPE,
 
             // Two or more fingers moving in arbitrary directions, button is not pressed.
             // Pointer does not move.
             // Emits DOWN, POINTER_DOWN, MOVE, POINTER_UP and UP events that follow
             // each finger individually relative to the initial centroid of the finger.
-            // The centroid is fixed when entering this state.
             FREEFORM,
 
             // Waiting for quiet time to end before starting the next gesture.
             QUIET,
         };
 
+        // Time the first finger went down.
+        nsecs_t firstTouchTime;
+
         // The active pointer id from the raw touch data.
         int32_t activeTouchId; // -1 if none
 
@@ -959,32 +979,20 @@
 
         // Pointer coords and ids for the current and previous pointer gesture.
         Mode currentGestureMode;
-        uint32_t currentGesturePointerCount;
         BitSet32 currentGestureIdBits;
         uint32_t currentGestureIdToIndex[MAX_POINTER_ID + 1];
         PointerCoords currentGestureCoords[MAX_POINTERS];
 
         Mode lastGestureMode;
-        uint32_t lastGesturePointerCount;
         BitSet32 lastGestureIdBits;
         uint32_t lastGestureIdToIndex[MAX_POINTER_ID + 1];
         PointerCoords lastGestureCoords[MAX_POINTERS];
 
-        // Tracks for all pointers originally went down.
-        TouchData touchOrigin;
-
-        // Describes how touch ids are mapped to gesture ids for freeform gestures.
-        uint32_t freeformTouchToGestureIdMap[MAX_POINTER_ID + 1];
-
-        // Initial centroid of the movement.
-        // Used to calculate how far the touch pointers have moved since the gesture started.
-        int32_t initialCentroidX;
-        int32_t initialCentroidY;
-
-        // Initial pointer location.
-        // Used to track where the pointer was when the gesture started.
-        float initialPointerX;
-        float initialPointerY;
+        // Pointer coords and ids for the current spots.
+        PointerControllerInterface::SpotGesture spotGesture;
+        BitSet32 spotIdBits; // same set of ids as touch ids
+        uint32_t spotIdToIndex[MAX_POINTER_ID + 1];
+        PointerCoords spotCoords[MAX_POINTERS];
 
         // Time the pointer gesture last went down.
         nsecs_t downTime;
@@ -992,26 +1000,34 @@
         // Time we started waiting for a tap gesture.
         nsecs_t tapTime;
 
+        // Location of initial tap.
+        float tapX, tapY;
+
         // Time we started waiting for quiescence.
         nsecs_t quietTime;
 
+        // Reference points for multitouch gestures.
+        float referenceTouchX;    // reference touch X/Y coordinates in surface units
+        float referenceTouchY;
+        float referenceGestureX;  // reference gesture X/Y coordinates in pixels
+        float referenceGestureY;
+
+        // Describes how touch ids are mapped to gesture ids for freeform gestures.
+        uint32_t freeformTouchToGestureIdMap[MAX_POINTER_ID + 1];
+
         // A velocity tracker for determining whether to switch active pointers during drags.
         VelocityTracker velocityTracker;
 
         void reset() {
+            firstTouchTime = LLONG_MIN;
             activeTouchId = -1;
             activeGestureId = -1;
             currentGestureMode = NEUTRAL;
-            currentGesturePointerCount = 0;
             currentGestureIdBits.clear();
             lastGestureMode = NEUTRAL;
-            lastGesturePointerCount = 0;
             lastGestureIdBits.clear();
-            touchOrigin.clear();
-            initialCentroidX = 0;
-            initialCentroidY = 0;
-            initialPointerX = 0;
-            initialPointerY = 0;
+            spotGesture = PointerControllerInterface::SPOT_GESTURE_NEUTRAL;
+            spotIdBits.clear();
             downTime = 0;
             velocityTracker.clear();
             resetTapTime();
@@ -1035,6 +1051,7 @@
     void dispatchPointerGestures(nsecs_t when, uint32_t policyFlags);
     void preparePointerGestures(nsecs_t when,
             bool* outCancelPreviousGesture, bool* outFinishPreviousGesture);
+    void moveSpotsLocked();
 
     // Dispatches a motion event.
     // If the changedId is >= 0 and the action is POINTER_DOWN or POINTER_UP, the
diff --git a/services/input/PointerController.cpp b/services/input/PointerController.cpp
index 15effb7..ffef720 100644
--- a/services/input/PointerController.cpp
+++ b/services/input/PointerController.cpp
@@ -36,40 +36,49 @@
 // --- PointerController ---
 
 // Time to wait before starting the fade when the pointer is inactive.
-static const nsecs_t INACTIVITY_FADE_DELAY_TIME_NORMAL = 15 * 1000 * 1000000LL; // 15 seconds
-static const nsecs_t INACTIVITY_FADE_DELAY_TIME_SHORT = 3 * 1000 * 1000000LL; // 3 seconds
+static const nsecs_t INACTIVITY_TIMEOUT_DELAY_TIME_NORMAL = 15 * 1000 * 1000000LL; // 15 seconds
+static const nsecs_t INACTIVITY_TIMEOUT_DELAY_TIME_SHORT = 3 * 1000 * 1000000LL; // 3 seconds
+
+// Time to wait between animation frames.
+static const nsecs_t ANIMATION_FRAME_INTERVAL = 1000000000LL / 60;
+
+// Time to spend fading out the spot completely.
+static const nsecs_t SPOT_FADE_DURATION = 200 * 1000000LL; // 200 ms
 
 // Time to spend fading out the pointer completely.
-static const nsecs_t FADE_DURATION = 500 * 1000000LL; // 500 ms
-
-// Time to wait between frames.
-static const nsecs_t FADE_FRAME_INTERVAL = 1000000000LL / 60;
-
-// Amount to subtract from alpha per frame.
-static const float FADE_DECAY_PER_FRAME = float(FADE_FRAME_INTERVAL) / FADE_DURATION;
+static const nsecs_t POINTER_FADE_DURATION = 500 * 1000000LL; // 500 ms
 
 
-PointerController::PointerController(const sp<Looper>& looper,
-        const sp<SpriteController>& spriteController) :
-        mLooper(looper), mSpriteController(spriteController) {
+// --- PointerController ---
+
+PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy,
+        const sp<Looper>& looper, const sp<SpriteController>& spriteController) :
+        mPolicy(policy), mLooper(looper), mSpriteController(spriteController) {
     mHandler = new WeakMessageHandler(this);
 
     AutoMutex _l(mLock);
 
+    mLocked.animationPending = false;
+
     mLocked.displayWidth = -1;
     mLocked.displayHeight = -1;
     mLocked.displayOrientation = DISPLAY_ORIENTATION_0;
 
+    mLocked.presentation = PRESENTATION_POINTER;
+    mLocked.presentationChanged = false;
+
+    mLocked.inactivityTimeout = INACTIVITY_TIMEOUT_NORMAL;
+
+    mLocked.pointerIsFading = true; // keep the pointer initially faded
     mLocked.pointerX = 0;
     mLocked.pointerY = 0;
+    mLocked.pointerAlpha = 0.0f;
+    mLocked.pointerSprite = mSpriteController->createSprite();
+    mLocked.pointerIconChanged = false;
+
     mLocked.buttonState = 0;
 
-    mLocked.fadeAlpha = 1;
-    mLocked.inactivityFadeDelay = INACTIVITY_FADE_DELAY_NORMAL;
-
-    mLocked.visible = false;
-
-    mLocked.sprite = mSpriteController->createSprite();
+    loadResources();
 }
 
 PointerController::~PointerController() {
@@ -77,7 +86,13 @@
 
     AutoMutex _l(mLock);
 
-    mLocked.sprite.clear();
+    mLocked.pointerSprite.clear();
+
+    for (size_t i = 0; i < mLocked.spots.size(); i++) {
+        delete mLocked.spots.itemAt(i);
+    }
+    mLocked.spots.clear();
+    mLocked.recycledSprites.clear();
 }
 
 bool PointerController::getBounds(float* outMinX, float* outMinY,
@@ -130,8 +145,6 @@
 
     if (mLocked.buttonState != buttonState) {
         mLocked.buttonState = buttonState;
-        unfadeBeforeUpdateLocked();
-        updateLocked();
     }
 }
 
@@ -167,8 +180,7 @@
         } else {
             mLocked.pointerY = y;
         }
-        unfadeBeforeUpdateLocked();
-        updateLocked();
+        updatePointerLocked();
     }
 }
 
@@ -182,32 +194,105 @@
 void PointerController::fade() {
     AutoMutex _l(mLock);
 
-    startFadeLocked();
+    sendImmediateInactivityTimeoutLocked();
 }
 
 void PointerController::unfade() {
     AutoMutex _l(mLock);
 
-    if (unfadeBeforeUpdateLocked()) {
-        updateLocked();
+    // Always reset the inactivity timer.
+    resetInactivityTimeoutLocked();
+
+    // Unfade immediately if needed.
+    if (mLocked.pointerIsFading) {
+        mLocked.pointerIsFading = false;
+        mLocked.pointerAlpha = 1.0f;
+        updatePointerLocked();
     }
 }
 
-void PointerController::setInactivityFadeDelay(InactivityFadeDelay inactivityFadeDelay) {
+void PointerController::setPresentation(Presentation presentation) {
     AutoMutex _l(mLock);
 
-    if (mLocked.inactivityFadeDelay != inactivityFadeDelay) {
-        mLocked.inactivityFadeDelay = inactivityFadeDelay;
-        startInactivityFadeDelayLocked();
+    if (mLocked.presentation != presentation) {
+        mLocked.presentation = presentation;
+        mLocked.presentationChanged = true;
+
+        if (presentation != PRESENTATION_SPOT) {
+            fadeOutAndReleaseAllSpotsLocked();
+        }
+
+        updatePointerLocked();
     }
 }
 
-void PointerController::updateLocked() {
-    mLocked.sprite->openTransaction();
-    mLocked.sprite->setPosition(mLocked.pointerX, mLocked.pointerY);
-    mLocked.sprite->setAlpha(mLocked.fadeAlpha);
-    mLocked.sprite->setVisible(mLocked.visible);
-    mLocked.sprite->closeTransaction();
+void PointerController::setSpots(SpotGesture spotGesture,
+        const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, BitSet32 spotIdBits) {
+#if DEBUG_POINTER_UPDATES
+    LOGD("setSpots: spotGesture=%d", spotGesture);
+    for (BitSet32 idBits(spotIdBits); !idBits.isEmpty(); ) {
+        uint32_t id = idBits.firstMarkedBit();
+        idBits.clearBit(id);
+        const PointerCoords& c = spotCoords[spotIdToIndex[id]];
+        LOGD("  spot %d: position=(%0.3f, %0.3f), pressure=%0.3f", id,
+                c.getAxisValue(AMOTION_EVENT_AXIS_X),
+                c.getAxisValue(AMOTION_EVENT_AXIS_Y),
+                c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE));
+    }
+#endif
+
+    AutoMutex _l(mLock);
+
+    mSpriteController->openTransaction();
+
+    // Add or move spots for fingers that are down.
+    for (BitSet32 idBits(spotIdBits); !idBits.isEmpty(); ) {
+        uint32_t id = idBits.firstMarkedBit();
+        idBits.clearBit(id);
+
+        const PointerCoords& c = spotCoords[spotIdToIndex[id]];
+        const SpriteIcon& icon = c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE) > 0
+                ? mResources.spotTouch : mResources.spotHover;
+        float x = c.getAxisValue(AMOTION_EVENT_AXIS_X);
+        float y = c.getAxisValue(AMOTION_EVENT_AXIS_Y);
+
+        Spot* spot = getSpotLocked(id);
+        if (!spot) {
+            spot = createAndAddSpotLocked(id);
+        }
+
+        spot->updateSprite(&icon, x, y);
+    }
+
+    // Remove spots for fingers that went up.
+    for (size_t i = 0; i < mLocked.spots.size(); i++) {
+        Spot* spot = mLocked.spots.itemAt(i);
+        if (spot->id != Spot::INVALID_ID
+                && !spotIdBits.hasBit(spot->id)) {
+            fadeOutAndReleaseSpotLocked(spot);
+        }
+    }
+
+    mSpriteController->closeTransaction();
+}
+
+void PointerController::clearSpots() {
+#if DEBUG_POINTER_UPDATES
+    LOGD("clearSpots");
+#endif
+
+    AutoMutex _l(mLock);
+
+    fadeOutAndReleaseAllSpotsLocked();
+}
+
+void PointerController::setInactivityTimeout(InactivityTimeout inactivityTimeout) {
+    AutoMutex _l(mLock);
+
+    if (mLocked.inactivityTimeout != inactivityTimeout) {
+        mLocked.inactivityTimeout = inactivityTimeout;
+        resetInactivityTimeoutLocked();
+    }
 }
 
 void PointerController::setDisplaySize(int32_t width, int32_t height) {
@@ -226,7 +311,8 @@
             mLocked.pointerY = 0;
         }
 
-        updateLocked();
+        fadeOutAndReleaseAllSpotsLocked();
+        updatePointerLocked();
     }
 }
 
@@ -283,74 +369,217 @@
         mLocked.pointerY = y - 0.5f;
         mLocked.displayOrientation = orientation;
 
-        updateLocked();
+        updatePointerLocked();
     }
 }
 
-void PointerController::setPointerIcon(const SkBitmap* bitmap, float hotSpotX, float hotSpotY) {
+void PointerController::setPointerIcon(const SpriteIcon& icon) {
     AutoMutex _l(mLock);
 
-    mLocked.sprite->setBitmap(bitmap, hotSpotX, hotSpotY);
+    mLocked.pointerIcon = icon.copy();
+    mLocked.pointerIconChanged = true;
+
+    updatePointerLocked();
 }
 
 void PointerController::handleMessage(const Message& message) {
     switch (message.what) {
-    case MSG_FADE_STEP: {
-        AutoMutex _l(mLock);
-        fadeStepLocked();
+    case MSG_ANIMATE:
+        doAnimate();
+        break;
+    case MSG_INACTIVITY_TIMEOUT:
+        doInactivityTimeout();
         break;
     }
-    }
 }
 
-bool PointerController::unfadeBeforeUpdateLocked() {
-    sendFadeStepMessageDelayedLocked(getInactivityFadeDelayTimeLocked());
+void PointerController::doAnimate() {
+    AutoMutex _l(mLock);
 
-    if (isFadingLocked()) {
-        mLocked.visible = true;
-        mLocked.fadeAlpha = 1;
-        return true; // update required to effect the unfade
-    }
-    return false; // update not required
-}
+    bool keepAnimating = false;
+    mLocked.animationPending = false;
+    nsecs_t frameDelay = systemTime(SYSTEM_TIME_MONOTONIC) - mLocked.animationTime;
 
-void PointerController::startFadeLocked() {
-    if (!isFadingLocked()) {
-        sendFadeStepMessageDelayedLocked(0);
-    }
-}
-
-void PointerController::startInactivityFadeDelayLocked() {
-    if (!isFadingLocked()) {
-        sendFadeStepMessageDelayedLocked(getInactivityFadeDelayTimeLocked());
-    }
-}
-
-void PointerController::fadeStepLocked() {
-    if (mLocked.visible) {
-        mLocked.fadeAlpha -= FADE_DECAY_PER_FRAME;
-        if (mLocked.fadeAlpha < 0) {
-            mLocked.fadeAlpha = 0;
-            mLocked.visible = false;
+    // Animate pointer fade.
+    if (mLocked.pointerIsFading) {
+        mLocked.pointerAlpha -= float(frameDelay) / POINTER_FADE_DURATION;
+        if (mLocked.pointerAlpha <= 0) {
+            mLocked.pointerAlpha = 0;
         } else {
-            sendFadeStepMessageDelayedLocked(FADE_FRAME_INTERVAL);
+            keepAnimating = true;
         }
-        updateLocked();
+        updatePointerLocked();
+    }
+
+    // Animate spots that are fading out and being removed.
+    for (size_t i = 0; i < mLocked.spots.size(); i++) {
+        Spot* spot = mLocked.spots.itemAt(i);
+        if (spot->id == Spot::INVALID_ID) {
+            spot->alpha -= float(frameDelay) / SPOT_FADE_DURATION;
+            if (spot->alpha <= 0) {
+                mLocked.spots.removeAt(i--);
+                releaseSpotLocked(spot);
+            } else {
+                spot->sprite->setAlpha(spot->alpha);
+                keepAnimating = true;
+            }
+        }
+    }
+
+    if (keepAnimating) {
+        startAnimationLocked();
     }
 }
 
-bool PointerController::isFadingLocked() {
-    return !mLocked.visible || mLocked.fadeAlpha != 1;
+void PointerController::doInactivityTimeout() {
+    AutoMutex _l(mLock);
+
+    if (!mLocked.pointerIsFading) {
+        mLocked.pointerIsFading = true;
+        startAnimationLocked();
+    }
 }
 
-nsecs_t PointerController::getInactivityFadeDelayTimeLocked() {
-    return mLocked.inactivityFadeDelay == INACTIVITY_FADE_DELAY_SHORT
-            ? INACTIVITY_FADE_DELAY_TIME_SHORT : INACTIVITY_FADE_DELAY_TIME_NORMAL;
+void PointerController::startAnimationLocked() {
+    if (!mLocked.animationPending) {
+        mLocked.animationPending = true;
+        mLocked.animationTime = systemTime(SYSTEM_TIME_MONOTONIC);
+        mLooper->sendMessageDelayed(ANIMATION_FRAME_INTERVAL, mHandler, Message(MSG_ANIMATE));
+    }
 }
 
-void PointerController::sendFadeStepMessageDelayedLocked(nsecs_t delayTime) {
-    mLooper->removeMessages(mHandler, MSG_FADE_STEP);
-    mLooper->sendMessageDelayed(delayTime, mHandler, Message(MSG_FADE_STEP));
+void PointerController::resetInactivityTimeoutLocked() {
+    mLooper->removeMessages(mHandler, MSG_INACTIVITY_TIMEOUT);
+
+    nsecs_t timeout = mLocked.inactivityTimeout == INACTIVITY_TIMEOUT_SHORT
+            ? INACTIVITY_TIMEOUT_DELAY_TIME_SHORT : INACTIVITY_TIMEOUT_DELAY_TIME_NORMAL;
+    mLooper->sendMessageDelayed(timeout, mHandler, MSG_INACTIVITY_TIMEOUT);
+}
+
+void PointerController::sendImmediateInactivityTimeoutLocked() {
+    mLooper->removeMessages(mHandler, MSG_INACTIVITY_TIMEOUT);
+    mLooper->sendMessage(mHandler, MSG_INACTIVITY_TIMEOUT);
+}
+
+void PointerController::updatePointerLocked() {
+    mSpriteController->openTransaction();
+
+    mLocked.pointerSprite->setLayer(Sprite::BASE_LAYER_POINTER);
+    mLocked.pointerSprite->setPosition(mLocked.pointerX, mLocked.pointerY);
+
+    if (mLocked.pointerAlpha > 0) {
+        mLocked.pointerSprite->setAlpha(mLocked.pointerAlpha);
+        mLocked.pointerSprite->setVisible(true);
+    } else {
+        mLocked.pointerSprite->setVisible(false);
+    }
+
+    if (mLocked.pointerIconChanged || mLocked.presentationChanged) {
+        mLocked.pointerSprite->setIcon(mLocked.presentation == PRESENTATION_POINTER
+                ? mLocked.pointerIcon : mResources.spotAnchor);
+        mLocked.pointerIconChanged = false;
+        mLocked.presentationChanged = false;
+    }
+
+    mSpriteController->closeTransaction();
+}
+
+PointerController::Spot* PointerController::getSpotLocked(uint32_t id) {
+    for (size_t i = 0; i < mLocked.spots.size(); i++) {
+        Spot* spot = mLocked.spots.itemAt(i);
+        if (spot->id == id) {
+            return spot;
+        }
+    }
+    return NULL;
+}
+
+PointerController::Spot* PointerController::createAndAddSpotLocked(uint32_t id) {
+    // Remove spots until we have fewer than MAX_SPOTS remaining.
+    while (mLocked.spots.size() >= MAX_SPOTS) {
+        Spot* spot = removeFirstFadingSpotLocked();
+        if (!spot) {
+            spot = mLocked.spots.itemAt(0);
+            mLocked.spots.removeAt(0);
+        }
+        releaseSpotLocked(spot);
+    }
+
+    // Obtain a sprite from the recycled pool.
+    sp<Sprite> sprite;
+    if (! mLocked.recycledSprites.isEmpty()) {
+        sprite = mLocked.recycledSprites.top();
+        mLocked.recycledSprites.pop();
+    } else {
+        sprite = mSpriteController->createSprite();
+    }
+
+    // Return the new spot.
+    Spot* spot = new Spot(id, sprite);
+    mLocked.spots.push(spot);
+    return spot;
+}
+
+PointerController::Spot* PointerController::removeFirstFadingSpotLocked() {
+    for (size_t i = 0; i < mLocked.spots.size(); i++) {
+        Spot* spot = mLocked.spots.itemAt(i);
+        if (spot->id == Spot::INVALID_ID) {
+            mLocked.spots.removeAt(i);
+            return spot;
+        }
+    }
+    return NULL;
+}
+
+void PointerController::releaseSpotLocked(Spot* spot) {
+    spot->sprite->clearIcon();
+
+    if (mLocked.recycledSprites.size() < MAX_RECYCLED_SPRITES) {
+        mLocked.recycledSprites.push(spot->sprite);
+    }
+
+    delete spot;
+}
+
+void PointerController::fadeOutAndReleaseSpotLocked(Spot* spot) {
+    if (spot->id != Spot::INVALID_ID) {
+        spot->id = Spot::INVALID_ID;
+        startAnimationLocked();
+    }
+}
+
+void PointerController::fadeOutAndReleaseAllSpotsLocked() {
+    for (size_t i = 0; i < mLocked.spots.size(); i++) {
+        Spot* spot = mLocked.spots.itemAt(i);
+        fadeOutAndReleaseSpotLocked(spot);
+    }
+}
+
+void PointerController::loadResources() {
+    mPolicy->loadPointerResources(&mResources);
+}
+
+
+// --- PointerController::Spot ---
+
+void PointerController::Spot::updateSprite(const SpriteIcon* icon, float x, float y) {
+    sprite->setLayer(Sprite::BASE_LAYER_SPOT + id);
+    sprite->setAlpha(alpha);
+    sprite->setTransformationMatrix(SpriteTransformationMatrix(scale, 0.0f, 0.0f, scale));
+    sprite->setPosition(x, y);
+
+    this->x = x;
+    this->y = y;
+
+    if (icon != lastIcon) {
+        lastIcon = icon;
+        if (icon) {
+            sprite->setIcon(*icon);
+            sprite->setVisible(true);
+        } else {
+            sprite->setVisible(false);
+        }
+    }
 }
 
 } // namespace android
diff --git a/services/input/PointerController.h b/services/input/PointerController.h
index d467a5a..afd6371 100644
--- a/services/input/PointerController.h
+++ b/services/input/PointerController.h
@@ -30,7 +30,10 @@
 namespace android {
 
 /**
- * Interface for tracking a single (mouse) pointer.
+ * Interface for tracking a mouse / touch pad pointer and touch pad spots.
+ *
+ * The spots are sprites on screen that visually represent the positions of
+ * fingers
  *
  * The pointer controller is responsible for providing synchronization and for tracking
  * display orientation changes if needed.
@@ -64,8 +67,95 @@
     /* Fades the pointer out now. */
     virtual void fade() = 0;
 
-    /* Makes the pointer visible if it has faded out. */
+    /* Makes the pointer visible if it has faded out.
+     * The pointer never unfades itself automatically.  This method must be called
+     * by the client whenever the pointer is moved or a button is pressed and it
+     * wants to ensure that the pointer becomes visible again. */
     virtual void unfade() = 0;
+
+    enum Presentation {
+        // Show the mouse pointer.
+        PRESENTATION_POINTER,
+        // Show spots and a spot anchor in place of the mouse pointer.
+        PRESENTATION_SPOT,
+    };
+
+    /* Sets the mode of the pointer controller. */
+    virtual void setPresentation(Presentation presentation) = 0;
+
+    // Describes the current gesture.
+    enum SpotGesture {
+        // No gesture.
+        // Do not display any spots.
+        SPOT_GESTURE_NEUTRAL,
+        // Tap at current location.
+        // Briefly display one spot at the tapped location.
+        SPOT_GESTURE_TAP,
+        // Button pressed but no finger is down.
+        // Display spot at pressed location.
+        SPOT_GESTURE_BUTTON_CLICK,
+        // Button pressed and a finger is down.
+        // Display spot at pressed location.
+        SPOT_GESTURE_BUTTON_DRAG,
+        // One finger down and hovering.
+        // Display spot at the hovered location.
+        SPOT_GESTURE_HOVER,
+        // Two fingers down but not sure in which direction they are moving so we consider
+        // it a press at the pointer location.
+        // Display two spots near the pointer location.
+        SPOT_GESTURE_PRESS,
+        // Two fingers down and moving in same direction.
+        // Display two spots near the pointer location.
+        SPOT_GESTURE_SWIPE,
+        // Two or more fingers down and moving in arbitrary directions.
+        // Display two or more spots near the pointer location, one for each finger.
+        SPOT_GESTURE_FREEFORM,
+    };
+
+    /* Sets the spots for the current gesture.
+     * The spots are not subject to the inactivity timeout like the pointer
+     * itself it since they are expected to remain visible for so long as
+     * the fingers are on the touch pad.
+     *
+     * The values of the AMOTION_EVENT_AXIS_PRESSURE axis is significant.
+     * For spotCoords, pressure != 0 indicates that the spot's location is being
+     * pressed (not hovering).
+     */
+    virtual void setSpots(SpotGesture spotGesture,
+            const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
+            BitSet32 spotIdBits) = 0;
+
+    /* Removes all spots. */
+    virtual void clearSpots() = 0;
+};
+
+
+/*
+ * Pointer resources.
+ */
+struct PointerResources {
+    SpriteIcon spotHover;
+    SpriteIcon spotTouch;
+    SpriteIcon spotAnchor;
+};
+
+
+/*
+ * Pointer controller policy interface.
+ *
+ * The pointer controller policy is used by the pointer controller to interact with
+ * the Window Manager and other system components.
+ *
+ * The actual implementation is partially supported by callbacks into the DVM
+ * via JNI.  This interface is also mocked in the unit tests.
+ */
+class PointerControllerPolicyInterface : public virtual RefBase {
+protected:
+    PointerControllerPolicyInterface() { }
+    virtual ~PointerControllerPolicyInterface() { }
+
+public:
+    virtual void loadPointerResources(PointerResources* outResources) = 0;
 };
 
 
@@ -79,12 +169,13 @@
     virtual ~PointerController();
 
 public:
-    enum InactivityFadeDelay {
-        INACTIVITY_FADE_DELAY_NORMAL = 0,
-        INACTIVITY_FADE_DELAY_SHORT = 1,
+    enum InactivityTimeout {
+        INACTIVITY_TIMEOUT_NORMAL = 0,
+        INACTIVITY_TIMEOUT_SHORT = 1,
     };
 
-    PointerController(const sp<Looper>& looper, const sp<SpriteController>& spriteController);
+    PointerController(const sp<PointerControllerPolicyInterface>& policy,
+            const sp<Looper>& looper, const sp<SpriteController>& spriteController);
 
     virtual bool getBounds(float* outMinX, float* outMinY,
             float* outMaxX, float* outMaxY) const;
@@ -96,51 +187,101 @@
     virtual void fade();
     virtual void unfade();
 
+    virtual void setPresentation(Presentation presentation);
+    virtual void setSpots(SpotGesture spotGesture,
+            const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, BitSet32 spotIdBits);
+    virtual void clearSpots();
+
     void setDisplaySize(int32_t width, int32_t height);
     void setDisplayOrientation(int32_t orientation);
-    void setPointerIcon(const SkBitmap* bitmap, float hotSpotX, float hotSpotY);
-    void setInactivityFadeDelay(InactivityFadeDelay inactivityFadeDelay);
+    void setPointerIcon(const SpriteIcon& icon);
+    void setInactivityTimeout(InactivityTimeout inactivityTimeout);
 
 private:
+    static const size_t MAX_RECYCLED_SPRITES = 12;
+    static const size_t MAX_SPOTS = 12;
+
     enum {
-        MSG_FADE_STEP = 0,
+        MSG_ANIMATE,
+        MSG_INACTIVITY_TIMEOUT,
+    };
+
+    struct Spot {
+        static const uint32_t INVALID_ID = 0xffffffff;
+
+        uint32_t id;
+        sp<Sprite> sprite;
+        float alpha;
+        float scale;
+        float x, y;
+
+        inline Spot(uint32_t id, const sp<Sprite>& sprite)
+                : id(id), sprite(sprite), alpha(1.0f), scale(1.0f),
+                  x(0.0f), y(0.0f), lastIcon(NULL) { }
+
+        void updateSprite(const SpriteIcon* icon, float x, float y);
+
+    private:
+        const SpriteIcon* lastIcon;
     };
 
     mutable Mutex mLock;
 
+    sp<PointerControllerPolicyInterface> mPolicy;
     sp<Looper> mLooper;
     sp<SpriteController> mSpriteController;
     sp<WeakMessageHandler> mHandler;
 
+    PointerResources mResources;
+
     struct Locked {
+        bool animationPending;
+        nsecs_t animationTime;
+
         int32_t displayWidth;
         int32_t displayHeight;
         int32_t displayOrientation;
 
+        InactivityTimeout inactivityTimeout;
+
+        Presentation presentation;
+        bool presentationChanged;
+
+        bool pointerIsFading;
         float pointerX;
         float pointerY;
+        float pointerAlpha;
+        sp<Sprite> pointerSprite;
+        SpriteIcon pointerIcon;
+        bool pointerIconChanged;
+
         uint32_t buttonState;
 
-        float fadeAlpha;
-        InactivityFadeDelay inactivityFadeDelay;
-
-        bool visible;
-
-        sp<Sprite> sprite;
+        Vector<Spot*> spots;
+        Vector<sp<Sprite> > recycledSprites;
     } mLocked;
 
     bool getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
     void setPositionLocked(float x, float y);
-    void updateLocked();
 
     void handleMessage(const Message& message);
-    bool unfadeBeforeUpdateLocked();
-    void startFadeLocked();
-    void startInactivityFadeDelayLocked();
-    void fadeStepLocked();
-    bool isFadingLocked();
-    nsecs_t getInactivityFadeDelayTimeLocked();
-    void sendFadeStepMessageDelayedLocked(nsecs_t delayTime);
+    void doAnimate();
+    void doInactivityTimeout();
+
+    void startAnimationLocked();
+
+    void resetInactivityTimeoutLocked();
+    void sendImmediateInactivityTimeoutLocked();
+    void updatePointerLocked();
+
+    Spot* getSpotLocked(uint32_t id);
+    Spot* createAndAddSpotLocked(uint32_t id);
+    Spot* removeFirstFadingSpotLocked();
+    void releaseSpotLocked(Spot* spot);
+    void fadeOutAndReleaseSpotLocked(Spot* spot);
+    void fadeOutAndReleaseAllSpotsLocked();
+
+    void loadResources();
 };
 
 } // namespace android
diff --git a/services/input/SpotController.cpp b/services/input/SpotController.cpp
deleted file mode 100644
index dffad81..0000000
--- a/services/input/SpotController.cpp
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "SpotController"
-
-//#define LOG_NDEBUG 0
-
-// Log debug messages about spot updates
-#define DEBUG_SPOT_UPDATES 0
-
-#include "SpotController.h"
-
-#include <cutils/log.h>
-
-namespace android {
-
-// --- SpotController ---
-
-SpotController::SpotController(const sp<Looper>& looper,
-        const sp<SpriteController>& spriteController) :
-        mLooper(looper), mSpriteController(spriteController) {
-    mHandler = new WeakMessageHandler(this);
-}
-
-SpotController::~SpotController() {
-    mLooper->removeMessages(mHandler);
-}
-
-void SpotController:: handleMessage(const Message& message) {
-}
-
-} // namespace android
diff --git a/services/input/SpotController.h b/services/input/SpotController.h
deleted file mode 100644
index 1d091d7..0000000
--- a/services/input/SpotController.h
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef _UI_SPOT_CONTROLLER_H
-#define _UI_SPOT_CONTROLLER_H
-
-#include "SpriteController.h"
-
-#include <utils/RefBase.h>
-#include <utils/Looper.h>
-
-#include <SkBitmap.h>
-
-namespace android {
-
-/*
- * Interface for displaying spots on screen that visually represent the positions
- * of fingers on a touch pad.
- *
- * The spot controller is responsible for providing synchronization and for tracking
- * display orientation changes if needed.
- */
-class SpotControllerInterface : public virtual RefBase {
-protected:
-    SpotControllerInterface() { }
-    virtual ~SpotControllerInterface() { }
-
-public:
-
-};
-
-
-/*
- * Sprite-based spot controller implementation.
- */
-class SpotController : public SpotControllerInterface, public MessageHandler {
-protected:
-    virtual ~SpotController();
-
-public:
-    SpotController(const sp<Looper>& looper, const sp<SpriteController>& spriteController);
-
-private:
-    mutable Mutex mLock;
-
-    sp<Looper> mLooper;
-    sp<SpriteController> mSpriteController;
-    sp<WeakMessageHandler> mHandler;
-
-    struct Locked {
-    } mLocked;
-
-    void handleMessage(const Message& message);
-};
-
-} // namespace android
-
-#endif // _UI_SPOT_CONTROLLER_H
diff --git a/services/input/SpriteController.cpp b/services/input/SpriteController.cpp
index c6d4390f..2fd1f0a 100644
--- a/services/input/SpriteController.cpp
+++ b/services/input/SpriteController.cpp
@@ -36,6 +36,9 @@
 SpriteController::SpriteController(const sp<Looper>& looper, int32_t overlayLayer) :
         mLooper(looper), mOverlayLayer(overlayLayer) {
     mHandler = new WeakMessageHandler(this);
+
+    mLocked.transactionNestingCount = 0;
+    mLocked.deferredSpriteUpdate = false;
 }
 
 SpriteController::~SpriteController() {
@@ -51,17 +54,40 @@
     return new SpriteImpl(this);
 }
 
-void SpriteController::invalidateSpriteLocked(const sp<SpriteImpl>& sprite) {
-    bool wasEmpty = mInvalidatedSprites.isEmpty();
-    mInvalidatedSprites.push(sprite);
-    if (wasEmpty) {
+void SpriteController::openTransaction() {
+    AutoMutex _l(mLock);
+
+    mLocked.transactionNestingCount += 1;
+}
+
+void SpriteController::closeTransaction() {
+    AutoMutex _l(mLock);
+
+    LOG_ALWAYS_FATAL_IF(mLocked.transactionNestingCount == 0,
+            "Sprite closeTransaction() called but there is no open sprite transaction");
+
+    mLocked.transactionNestingCount -= 1;
+    if (mLocked.transactionNestingCount == 0 && mLocked.deferredSpriteUpdate) {
+        mLocked.deferredSpriteUpdate = false;
         mLooper->sendMessage(mHandler, Message(MSG_UPDATE_SPRITES));
     }
 }
 
+void SpriteController::invalidateSpriteLocked(const sp<SpriteImpl>& sprite) {
+    bool wasEmpty = mLocked.invalidatedSprites.isEmpty();
+    mLocked.invalidatedSprites.push(sprite);
+    if (wasEmpty) {
+        if (mLocked.transactionNestingCount != 0) {
+            mLocked.deferredSpriteUpdate = true;
+        } else {
+            mLooper->sendMessage(mHandler, Message(MSG_UPDATE_SPRITES));
+        }
+    }
+}
+
 void SpriteController::disposeSurfaceLocked(const sp<SurfaceControl>& surfaceControl) {
-    bool wasEmpty = mDisposedSurfaces.isEmpty();
-    mDisposedSurfaces.push(surfaceControl);
+    bool wasEmpty = mLocked.disposedSurfaces.isEmpty();
+    mLocked.disposedSurfaces.push(surfaceControl);
     if (wasEmpty) {
         mLooper->sendMessage(mHandler, Message(MSG_DISPOSE_SURFACES));
     }
@@ -89,14 +115,14 @@
     { // acquire lock
         AutoMutex _l(mLock);
 
-        numSprites = mInvalidatedSprites.size();
+        numSprites = mLocked.invalidatedSprites.size();
         for (size_t i = 0; i < numSprites; i++) {
-            const sp<SpriteImpl>& sprite = mInvalidatedSprites.itemAt(i);
+            const sp<SpriteImpl>& sprite = mLocked.invalidatedSprites.itemAt(i);
 
             updates.push(SpriteUpdate(sprite, sprite->getStateLocked()));
             sprite->resetDirtyLocked();
         }
-        mInvalidatedSprites.clear();
+        mLocked.invalidatedSprites.clear();
     } // release lock
 
     // Create missing surfaces.
@@ -105,8 +131,8 @@
         SpriteUpdate& update = updates.editItemAt(i);
 
         if (update.state.surfaceControl == NULL && update.state.wantSurfaceVisible()) {
-            update.state.surfaceWidth = update.state.bitmap.width();
-            update.state.surfaceHeight = update.state.bitmap.height();
+            update.state.surfaceWidth = update.state.icon.bitmap.width();
+            update.state.surfaceHeight = update.state.icon.bitmap.height();
             update.state.surfaceDrawn = false;
             update.state.surfaceVisible = false;
             update.state.surfaceControl = obtainSurface(
@@ -123,8 +149,8 @@
         SpriteUpdate& update = updates.editItemAt(i);
 
         if (update.state.surfaceControl != NULL && update.state.wantSurfaceVisible()) {
-            int32_t desiredWidth = update.state.bitmap.width();
-            int32_t desiredHeight = update.state.bitmap.height();
+            int32_t desiredWidth = update.state.icon.bitmap.width();
+            int32_t desiredHeight = update.state.icon.bitmap.height();
             if (update.state.surfaceWidth < desiredWidth
                     || update.state.surfaceHeight < desiredHeight) {
                 if (!haveGlobalTransaction) {
@@ -187,16 +213,16 @@
 
                 SkPaint paint;
                 paint.setXfermodeMode(SkXfermode::kSrc_Mode);
-                surfaceCanvas.drawBitmap(update.state.bitmap, 0, 0, &paint);
+                surfaceCanvas.drawBitmap(update.state.icon.bitmap, 0, 0, &paint);
 
-                if (surfaceInfo.w > uint32_t(update.state.bitmap.width())) {
+                if (surfaceInfo.w > uint32_t(update.state.icon.bitmap.width())) {
                     paint.setColor(0); // transparent fill color
-                    surfaceCanvas.drawRectCoords(update.state.bitmap.width(), 0,
-                            surfaceInfo.w, update.state.bitmap.height(), paint);
+                    surfaceCanvas.drawRectCoords(update.state.icon.bitmap.width(), 0,
+                            surfaceInfo.w, update.state.icon.bitmap.height(), paint);
                 }
-                if (surfaceInfo.h > uint32_t(update.state.bitmap.height())) {
+                if (surfaceInfo.h > uint32_t(update.state.icon.bitmap.height())) {
                     paint.setColor(0); // transparent fill color
-                    surfaceCanvas.drawRectCoords(0, update.state.bitmap.height(),
+                    surfaceCanvas.drawRectCoords(0, update.state.icon.bitmap.height(),
                             surfaceInfo.w, surfaceInfo.h, paint);
                 }
 
@@ -246,8 +272,8 @@
                     && (becomingVisible || (update.state.dirty & (DIRTY_POSITION
                             | DIRTY_HOTSPOT)))) {
                 status = update.state.surfaceControl->setPosition(
-                        update.state.positionX - update.state.hotSpotX,
-                        update.state.positionY - update.state.hotSpotY);
+                        update.state.positionX - update.state.icon.hotSpotX,
+                        update.state.positionY - update.state.icon.hotSpotY);
                 if (status) {
                     LOGE("Error %d setting sprite surface position.", status);
                 }
@@ -329,8 +355,10 @@
     // Collect disposed surfaces.
     Vector<sp<SurfaceControl> > disposedSurfaces;
     { // acquire lock
-        disposedSurfaces = mDisposedSurfaces;
-        mDisposedSurfaces.clear();
+        AutoMutex _l(mLock);
+
+        disposedSurfaces = mLocked.disposedSurfaces;
+        mLocked.disposedSurfaces.clear();
     } // release lock
 
     // Release the last reference to each surface outside of the lock.
@@ -349,7 +377,8 @@
 
     sp<SurfaceControl> surfaceControl = mSurfaceComposerClient->createSurface(
             getpid(), String8("Sprite"), 0, width, height, PIXEL_FORMAT_RGBA_8888);
-    if (surfaceControl == NULL) {
+    if (surfaceControl == NULL || !surfaceControl->isValid()
+            || !surfaceControl->getSurface()->isValid()) {
         LOGE("Error creating sprite surface.");
         return NULL;
     }
@@ -360,7 +389,7 @@
 // --- SpriteController::SpriteImpl ---
 
 SpriteController::SpriteImpl::SpriteImpl(const sp<SpriteController> controller) :
-        mController(controller), mTransactionNestingCount(0) {
+        mController(controller) {
 }
 
 SpriteController::SpriteImpl::~SpriteImpl() {
@@ -368,27 +397,33 @@
 
     // Let the controller take care of deleting the last reference to sprite
     // surfaces so that we do not block the caller on an IPC here.
-    if (mState.surfaceControl != NULL) {
-        mController->disposeSurfaceLocked(mState.surfaceControl);
-        mState.surfaceControl.clear();
+    if (mLocked.state.surfaceControl != NULL) {
+        mController->disposeSurfaceLocked(mLocked.state.surfaceControl);
+        mLocked.state.surfaceControl.clear();
     }
 }
 
-void SpriteController::SpriteImpl::setBitmap(const SkBitmap* bitmap,
-        float hotSpotX, float hotSpotY) {
+void SpriteController::SpriteImpl::setIcon(const SpriteIcon& icon) {
     AutoMutex _l(mController->mLock);
 
-    if (bitmap) {
-        bitmap->copyTo(&mState.bitmap, SkBitmap::kARGB_8888_Config);
-    } else {
-        mState.bitmap.reset();
-    }
+    uint32_t dirty;
+    if (icon.isValid()) {
+        icon.bitmap.copyTo(&mLocked.state.icon.bitmap, SkBitmap::kARGB_8888_Config);
 
-    uint32_t dirty = DIRTY_BITMAP;
-    if (mState.hotSpotX != hotSpotX || mState.hotSpotY != hotSpotY) {
-        mState.hotSpotX = hotSpotX;
-        mState.hotSpotY = hotSpotY;
-        dirty |= DIRTY_HOTSPOT;
+        if (!mLocked.state.icon.isValid()
+                || mLocked.state.icon.hotSpotX != icon.hotSpotX
+                || mLocked.state.icon.hotSpotY != icon.hotSpotY) {
+            mLocked.state.icon.hotSpotX = icon.hotSpotX;
+            mLocked.state.icon.hotSpotY = icon.hotSpotY;
+            dirty = DIRTY_BITMAP | DIRTY_HOTSPOT;
+        } else {
+            dirty = DIRTY_BITMAP;
+        }
+    } else if (mLocked.state.icon.isValid()) {
+        mLocked.state.icon.bitmap.reset();
+        dirty = DIRTY_BITMAP | DIRTY_HOTSPOT;
+    } else {
+        return; // setting to invalid icon and already invalid so nothing to do
     }
 
     invalidateLocked(dirty);
@@ -397,8 +432,8 @@
 void SpriteController::SpriteImpl::setVisible(bool visible) {
     AutoMutex _l(mController->mLock);
 
-    if (mState.visible != visible) {
-        mState.visible = visible;
+    if (mLocked.state.visible != visible) {
+        mLocked.state.visible = visible;
         invalidateLocked(DIRTY_VISIBILITY);
     }
 }
@@ -406,9 +441,9 @@
 void SpriteController::SpriteImpl::setPosition(float x, float y) {
     AutoMutex _l(mController->mLock);
 
-    if (mState.positionX != x || mState.positionY != y) {
-        mState.positionX = x;
-        mState.positionY = y;
+    if (mLocked.state.positionX != x || mLocked.state.positionY != y) {
+        mLocked.state.positionX = x;
+        mLocked.state.positionY = y;
         invalidateLocked(DIRTY_POSITION);
     }
 }
@@ -416,8 +451,8 @@
 void SpriteController::SpriteImpl::setLayer(int32_t layer) {
     AutoMutex _l(mController->mLock);
 
-    if (mState.layer != layer) {
-        mState.layer = layer;
+    if (mLocked.state.layer != layer) {
+        mLocked.state.layer = layer;
         invalidateLocked(DIRTY_LAYER);
     }
 }
@@ -425,8 +460,8 @@
 void SpriteController::SpriteImpl::setAlpha(float alpha) {
     AutoMutex _l(mController->mLock);
 
-    if (mState.alpha != alpha) {
-        mState.alpha = alpha;
+    if (mLocked.state.alpha != alpha) {
+        mLocked.state.alpha = alpha;
         invalidateLocked(DIRTY_ALPHA);
     }
 }
@@ -435,37 +470,18 @@
         const SpriteTransformationMatrix& matrix) {
     AutoMutex _l(mController->mLock);
 
-    if (mState.transformationMatrix != matrix) {
-        mState.transformationMatrix = matrix;
+    if (mLocked.state.transformationMatrix != matrix) {
+        mLocked.state.transformationMatrix = matrix;
         invalidateLocked(DIRTY_TRANSFORMATION_MATRIX);
     }
 }
 
-void SpriteController::SpriteImpl::openTransaction() {
-    AutoMutex _l(mController->mLock);
-
-    mTransactionNestingCount += 1;
-}
-
-void SpriteController::SpriteImpl::closeTransaction() {
-    AutoMutex _l(mController->mLock);
-
-    LOG_ALWAYS_FATAL_IF(mTransactionNestingCount == 0,
-            "Sprite closeTransaction() called but there is no open sprite transaction");
-
-    mTransactionNestingCount -= 1;
-    if (mTransactionNestingCount == 0 && mState.dirty) {
-        mController->invalidateSpriteLocked(this);
-    }
-}
-
 void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) {
-    if (mTransactionNestingCount > 0) {
-        bool wasDirty = mState.dirty;
-        mState.dirty |= dirty;
-        if (!wasDirty) {
-            mController->invalidateSpriteLocked(this);
-        }
+    bool wasDirty = mLocked.state.dirty;
+    mLocked.state.dirty |= dirty;
+
+    if (!wasDirty) {
+        mController->invalidateSpriteLocked(this);
     }
 }
 
diff --git a/services/input/SpriteController.h b/services/input/SpriteController.h
index 27afb5e..50ae8a5 100644
--- a/services/input/SpriteController.h
+++ b/services/input/SpriteController.h
@@ -33,6 +33,8 @@
  */
 struct SpriteTransformationMatrix {
     inline SpriteTransformationMatrix() : dsdx(1.0f), dtdx(0.0f), dsdy(0.0f), dtdy(1.0f) { }
+    inline SpriteTransformationMatrix(float dsdx, float dtdx, float dsdy, float dtdy) :
+            dsdx(dsdx), dtdx(dtdx), dsdy(dsdy), dtdy(dtdy) { }
 
     float dsdx;
     float dtdx;
@@ -52,6 +54,35 @@
 };
 
 /*
+ * Icon that a sprite displays, including its hotspot.
+ */
+struct SpriteIcon {
+    inline SpriteIcon() : hotSpotX(0), hotSpotY(0) { }
+    inline SpriteIcon(const SkBitmap& bitmap, float hotSpotX, float hotSpotY) :
+            bitmap(bitmap), hotSpotX(hotSpotX), hotSpotY(hotSpotY) { }
+
+    SkBitmap bitmap;
+    float hotSpotX;
+    float hotSpotY;
+
+    inline SpriteIcon copy() const {
+        SkBitmap bitmapCopy;
+        bitmap.copyTo(&bitmapCopy, SkBitmap::kARGB_8888_Config);
+        return SpriteIcon(bitmapCopy, hotSpotX, hotSpotY);
+    }
+
+    inline void reset() {
+        bitmap.reset();
+        hotSpotX = 0;
+        hotSpotY = 0;
+    }
+
+    inline bool isValid() const {
+        return !bitmap.isNull() && !bitmap.empty();
+    }
+};
+
+/*
  * A sprite is a simple graphical object that is displayed on-screen above other layers.
  * The basic sprite class is an interface.
  * The implementation is provided by the sprite controller.
@@ -62,9 +93,21 @@
     virtual ~Sprite() { }
 
 public:
+    enum {
+        // The base layer for pointer sprites.
+        BASE_LAYER_POINTER = 0, // reserve space for 1 pointer
+
+        // The base layer for spot sprites.
+        BASE_LAYER_SPOT = 1, // reserve space for MAX_POINTER_ID spots
+    };
+
     /* Sets the bitmap that is drawn by the sprite.
      * The sprite retains a copy of the bitmap for subsequent rendering. */
-    virtual void setBitmap(const SkBitmap* bitmap, float hotSpotX, float hotSpotY) = 0;
+    virtual void setIcon(const SpriteIcon& icon) = 0;
+
+    inline void clearIcon() {
+        setIcon(SpriteIcon());
+    }
 
     /* Sets whether the sprite is visible. */
     virtual void setVisible(bool visible) = 0;
@@ -81,14 +124,6 @@
 
     /* Sets the sprite transformation matrix. */
     virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix) = 0;
-
-    /* Opens or closes a transaction to perform a batch of sprite updates as part of
-     * a single operation such as setPosition and setAlpha.  It is not necessary to
-     * open a transaction when updating a single property.
-     * Calls to openTransaction() nest and must be matched by an equal number
-     * of calls to closeTransaction(). */
-    virtual void openTransaction() = 0;
-    virtual void closeTransaction() = 0;
 };
 
 /*
@@ -112,6 +147,14 @@
     /* Creates a new sprite, initially invisible. */
     sp<Sprite> createSprite();
 
+    /* Opens or closes a transaction to perform a batch of sprite updates as part of
+     * a single operation such as setPosition and setAlpha.  It is not necessary to
+     * open a transaction when updating a single property.
+     * Calls to openTransaction() nest and must be matched by an equal number
+     * of calls to closeTransaction(). */
+    void openTransaction();
+    void closeTransaction();
+
 private:
     enum {
         MSG_UPDATE_SPRITES,
@@ -135,16 +178,14 @@
      * Note that the SkBitmap holds a reference to a shared (and immutable) pixel ref. */
     struct SpriteState {
         inline SpriteState() :
-                dirty(0), hotSpotX(0), hotSpotY(0), visible(false),
+                dirty(0), visible(false),
                 positionX(0), positionY(0), layer(0), alpha(1.0f),
                 surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) {
         }
 
         uint32_t dirty;
 
-        SkBitmap bitmap;
-        float hotSpotX;
-        float hotSpotY;
+        SpriteIcon icon;
         bool visible;
         float positionX;
         float positionY;
@@ -159,7 +200,7 @@
         bool surfaceVisible;
 
         inline bool wantSurfaceVisible() const {
-            return visible && alpha > 0.0f && !bitmap.isNull() && !bitmap.empty();
+            return visible && alpha > 0.0f && icon.isValid();
         }
     };
 
@@ -177,37 +218,36 @@
     public:
         SpriteImpl(const sp<SpriteController> controller);
 
-        virtual void setBitmap(const SkBitmap* bitmap, float hotSpotX, float hotSpotY);
+        virtual void setIcon(const SpriteIcon& icon);
         virtual void setVisible(bool visible);
         virtual void setPosition(float x, float y);
         virtual void setLayer(int32_t layer);
         virtual void setAlpha(float alpha);
         virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix);
-        virtual void openTransaction();
-        virtual void closeTransaction();
 
         inline const SpriteState& getStateLocked() const {
-            return mState;
+            return mLocked.state;
         }
 
         inline void resetDirtyLocked() {
-            mState.dirty = 0;
+            mLocked.state.dirty = 0;
         }
 
         inline void setSurfaceLocked(const sp<SurfaceControl>& surfaceControl,
                 int32_t width, int32_t height, bool drawn, bool visible) {
-            mState.surfaceControl = surfaceControl;
-            mState.surfaceWidth = width;
-            mState.surfaceHeight = height;
-            mState.surfaceDrawn = drawn;
-            mState.surfaceVisible = visible;
+            mLocked.state.surfaceControl = surfaceControl;
+            mLocked.state.surfaceWidth = width;
+            mLocked.state.surfaceHeight = height;
+            mLocked.state.surfaceDrawn = drawn;
+            mLocked.state.surfaceVisible = visible;
         }
 
     private:
         sp<SpriteController> mController;
 
-        SpriteState mState; // guarded by mController->mLock
-        uint32_t mTransactionNestingCount; // guarded by mController->mLock
+        struct Locked {
+            SpriteState state;
+        } mLocked; // guarded by mController->mLock
 
         void invalidateLocked(uint32_t dirty);
     };
@@ -232,8 +272,12 @@
 
     sp<SurfaceComposerClient> mSurfaceComposerClient;
 
-    Vector<sp<SpriteImpl> > mInvalidatedSprites; // guarded by mLock
-    Vector<sp<SurfaceControl> > mDisposedSurfaces; // guarded by mLock
+    struct Locked {
+        Vector<sp<SpriteImpl> > invalidatedSprites;
+        Vector<sp<SurfaceControl> > disposedSurfaces;
+        uint32_t transactionNestingCount;
+        bool deferredSpriteUpdate;
+    } mLocked; // guarded by mLock
 
     void invalidateSpriteLocked(const sp<SpriteImpl>& sprite);
     void disposeSurfaceLocked(const sp<SurfaceControl>& surfaceControl);
diff --git a/services/input/tests/InputReader_test.cpp b/services/input/tests/InputReader_test.cpp
index ba8ca9c..54bb9d7 100644
--- a/services/input/tests/InputReader_test.cpp
+++ b/services/input/tests/InputReader_test.cpp
@@ -97,6 +97,16 @@
 
     virtual void unfade() {
     }
+
+    virtual void setPresentation(Presentation presentation) {
+    }
+
+    virtual void setSpots(SpotGesture spotGesture,
+            const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, BitSet32 spotIdBits) {
+    }
+
+    virtual void clearSpots() {
+    }
 };
 
 
@@ -192,10 +202,6 @@
     virtual sp<PointerControllerInterface> obtainPointerController(int32_t deviceId) {
         return mPointerControllers.valueFor(deviceId);
     }
-
-    virtual sp<SpotControllerInterface> obtainSpotController(int32_t device) {
-        return NULL;
-    }
 };
 
 
diff --git a/services/java/com/android/server/wm/InputManager.java b/services/java/com/android/server/wm/InputManager.java
index b0978a3..69bde41 100644
--- a/services/java/com/android/server/wm/InputManager.java
+++ b/services/java/com/android/server/wm/InputManager.java
@@ -23,12 +23,6 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
 import android.os.Environment;
 import android.os.Looper;
 import android.os.MessageQueue;
@@ -39,6 +33,7 @@
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.KeyEvent;
+import android.view.PointerIcon;
 import android.view.Surface;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
@@ -63,7 +58,8 @@
     private final Context mContext;
     private final WindowManagerService mWindowManagerService;
     
-    private static native void nativeInit(Callbacks callbacks, MessageQueue messageQueue);
+    private static native void nativeInit(Context context,
+            Callbacks callbacks, MessageQueue messageQueue);
     private static native void nativeStart();
     private static native void nativeSetDisplaySize(int displayId, int width, int height);
     private static native void nativeSetDisplayOrientation(int displayId, int rotation);
@@ -133,7 +129,7 @@
         Looper looper = windowManagerService.mH.getLooper();
 
         Slog.i(TAG, "Initializing input manager");
-        nativeInit(mCallbacks, looper.getQueue());
+        nativeInit(mContext, mCallbacks, looper.getQueue());
     }
     
     public void start() {
@@ -435,48 +431,6 @@
         }
     }
 
-    private static final class PointerIcon {
-        public Bitmap bitmap;
-        public float hotSpotX;
-        public float hotSpotY;
-
-        public static PointerIcon load(Resources resources, int resourceId) {
-            PointerIcon icon = new PointerIcon();
-
-            XmlResourceParser parser = resources.getXml(resourceId);
-            final int bitmapRes;
-            try {
-                XmlUtils.beginDocument(parser, "pointer-icon");
-
-                TypedArray a = resources.obtainAttributes(
-                        parser, com.android.internal.R.styleable.PointerIcon);
-                bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
-                icon.hotSpotX = a.getFloat(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
-                icon.hotSpotY = a.getFloat(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
-                a.recycle();
-            } catch (Exception ex) {
-                Slog.e(TAG, "Exception parsing pointer icon resource.", ex);
-                return null;
-            } finally {
-                parser.close();
-            }
-
-            if (bitmapRes == 0) {
-                Slog.e(TAG, "<pointer-icon> is missing bitmap attribute");
-                return null;
-            }
-
-            Drawable drawable = resources.getDrawable(bitmapRes);
-            if (!(drawable instanceof BitmapDrawable)) {
-                Slog.e(TAG, "<pointer-icon> bitmap attribute must refer to a bitmap drawable");
-                return null;
-            }
-
-            icon.bitmap = ((BitmapDrawable)drawable).getBitmap();
-            return icon;
-        }
-    }
-
     /*
      * Callbacks from native.
      */
@@ -641,8 +595,7 @@
 
         @SuppressWarnings("unused")
         public PointerIcon getPointerIcon() {
-            return PointerIcon.load(mContext.getResources(),
-                    com.android.internal.R.drawable.pointer_arrow_icon);
+            return PointerIcon.getDefaultIcon(mContext);
         }
     }
 }
diff --git a/services/jni/com_android_server_InputManager.cpp b/services/jni/com_android_server_InputManager.cpp
index aaa305e..1f10d9c 100644
--- a/services/jni/com_android_server_InputManager.cpp
+++ b/services/jni/com_android_server_InputManager.cpp
@@ -36,13 +36,13 @@
 
 #include <input/InputManager.h>
 #include <input/PointerController.h>
-#include <input/SpotController.h>
 #include <input/SpriteController.h>
 
 #include <android_os_MessageQueue.h>
 #include <android_view_KeyEvent.h>
 #include <android_view_MotionEvent.h>
 #include <android_view_InputChannel.h>
+#include <android_view_PointerIcon.h>
 #include <android/graphics/GraphicsJNI.h>
 
 #include "com_android_server_PowerManagerService.h"
@@ -101,12 +101,6 @@
     jfieldID navigation;
 } gConfigurationClassInfo;
 
-static struct {
-    jfieldID bitmap;
-    jfieldID hotSpotX;
-    jfieldID hotSpotY;
-} gPointerIconClassInfo;
-
 
 // --- Global functions ---
 
@@ -128,17 +122,30 @@
             getInputWindowHandleObjLocalRef(env);
 }
 
+static void loadSystemIconAsSprite(JNIEnv* env, jobject contextObj, int32_t style,
+        SpriteIcon* outSpriteIcon) {
+    PointerIcon pointerIcon;
+    status_t status = android_view_PointerIcon_loadSystemIcon(env,
+            contextObj, style, &pointerIcon);
+    if (!status) {
+        pointerIcon.bitmap.copyTo(&outSpriteIcon->bitmap, SkBitmap::kARGB_8888_Config);
+        outSpriteIcon->hotSpotX = pointerIcon.hotSpotX;
+        outSpriteIcon->hotSpotY = pointerIcon.hotSpotY;
+    }
+}
+
 
 // --- NativeInputManager ---
 
 class NativeInputManager : public virtual RefBase,
     public virtual InputReaderPolicyInterface,
-    public virtual InputDispatcherPolicyInterface {
+    public virtual InputDispatcherPolicyInterface,
+    public virtual PointerControllerPolicyInterface {
 protected:
     virtual ~NativeInputManager();
 
 public:
-    NativeInputManager(jobject callbacksObj, const sp<Looper>& looper);
+    NativeInputManager(jobject contextObj, jobject callbacksObj, const sp<Looper>& looper);
 
     inline sp<InputManager> getInputManager() const { return mInputManager; }
 
@@ -165,7 +172,6 @@
     virtual nsecs_t getVirtualKeyQuietTime();
     virtual void getExcludedDeviceNames(Vector<String8>& outExcludedDeviceNames);
     virtual sp<PointerControllerInterface> obtainPointerController(int32_t deviceId);
-    virtual sp<SpotControllerInterface> obtainSpotController(int32_t deviceId);
 
     /* --- InputDispatcherPolicyInterface implementation --- */
 
@@ -189,9 +195,14 @@
     virtual bool checkInjectEventsPermissionNonReentrant(
             int32_t injectorPid, int32_t injectorUid);
 
+    /* --- PointerControllerPolicyInterface implementation --- */
+
+    virtual void loadPointerResources(PointerResources* outResources);
+
 private:
     sp<InputManager> mInputManager;
 
+    jobject mContextObj;
     jobject mCallbacksObj;
     sp<Looper> mLooper;
 
@@ -223,7 +234,7 @@
         wp<PointerController> pointerController;
     } mLocked;
 
-    void updateInactivityFadeDelayLocked(const sp<PointerController>& controller);
+    void updateInactivityTimeoutLocked(const sp<PointerController>& controller);
     void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
     void ensureSpriteControllerLocked();
 
@@ -240,13 +251,15 @@
 
 
 
-NativeInputManager::NativeInputManager(jobject callbacksObj, const sp<Looper>& looper) :
+NativeInputManager::NativeInputManager(jobject contextObj,
+        jobject callbacksObj, const sp<Looper>& looper) :
         mLooper(looper),
         mFilterTouchEvents(-1), mFilterJumpyTouchEvents(-1), mVirtualKeyQuietTime(-1),
         mKeyRepeatTimeout(-1), mKeyRepeatDelay(-1),
         mMaxEventsPerSecond(-1) {
     JNIEnv* env = jniEnv();
 
+    mContextObj = env->NewGlobalRef(contextObj);
     mCallbacksObj = env->NewGlobalRef(callbacksObj);
 
     {
@@ -265,6 +278,7 @@
 NativeInputManager::~NativeInputManager() {
     JNIEnv* env = jniEnv();
 
+    env->DeleteGlobalRef(mContextObj);
     env->DeleteGlobalRef(mCallbacksObj);
 }
 
@@ -288,9 +302,13 @@
 
 void NativeInputManager::setDisplaySize(int32_t displayId, int32_t width, int32_t height) {
     if (displayId == 0) {
-        AutoMutex _l(mLock);
+        { // acquire lock
+            AutoMutex _l(mLock);
 
-        if (mLocked.displayWidth != width || mLocked.displayHeight != height) {
+            if (mLocked.displayWidth == width && mLocked.displayHeight == height) {
+                return;
+            }
+
             mLocked.displayWidth = width;
             mLocked.displayHeight = height;
 
@@ -298,7 +316,7 @@
             if (controller != NULL) {
                 controller->setDisplaySize(width, height);
             }
-        }
+        } // release lock
     }
 }
 
@@ -428,40 +446,33 @@
     if (controller == NULL) {
         ensureSpriteControllerLocked();
 
-        controller = new PointerController(mLooper, mLocked.spriteController);
+        controller = new PointerController(this, mLooper, mLocked.spriteController);
         mLocked.pointerController = controller;
 
         controller->setDisplaySize(mLocked.displayWidth, mLocked.displayHeight);
         controller->setDisplayOrientation(mLocked.displayOrientation);
 
         JNIEnv* env = jniEnv();
-        jobject iconObj = env->CallObjectMethod(mCallbacksObj, gCallbacksClassInfo.getPointerIcon);
-        if (!checkAndClearExceptionFromCallback(env, "getPointerIcon") && iconObj) {
-            jfloat iconHotSpotX = env->GetFloatField(iconObj, gPointerIconClassInfo.hotSpotX);
-            jfloat iconHotSpotY = env->GetFloatField(iconObj, gPointerIconClassInfo.hotSpotY);
-            jobject iconBitmapObj = env->GetObjectField(iconObj, gPointerIconClassInfo.bitmap);
-            if (iconBitmapObj) {
-                SkBitmap* iconBitmap = GraphicsJNI::getNativeBitmap(env, iconBitmapObj);
-                if (iconBitmap) {
-                    controller->setPointerIcon(iconBitmap, iconHotSpotX, iconHotSpotY);
-                }
-                env->DeleteLocalRef(iconBitmapObj);
+        jobject pointerIconObj = env->CallObjectMethod(mCallbacksObj,
+                gCallbacksClassInfo.getPointerIcon);
+        if (!checkAndClearExceptionFromCallback(env, "getPointerIcon")) {
+            PointerIcon pointerIcon;
+            status_t status = android_view_PointerIcon_load(env, pointerIconObj,
+                    mContextObj, &pointerIcon);
+            if (!status && !pointerIcon.isNullIcon()) {
+                controller->setPointerIcon(SpriteIcon(pointerIcon.bitmap,
+                        pointerIcon.hotSpotX, pointerIcon.hotSpotY));
+            } else {
+                controller->setPointerIcon(SpriteIcon());
             }
-            env->DeleteLocalRef(iconObj);
+            env->DeleteLocalRef(pointerIconObj);
         }
 
-        updateInactivityFadeDelayLocked(controller);
+        updateInactivityTimeoutLocked(controller);
     }
     return controller;
 }
 
-sp<SpotControllerInterface> NativeInputManager::obtainSpotController(int32_t deviceId) {
-    AutoMutex _l(mLock);
-
-    ensureSpriteControllerLocked();
-    return new SpotController(mLooper, mLocked.spriteController);
-}
-
 void NativeInputManager::ensureSpriteControllerLocked() {
     if (mLocked.spriteController == NULL) {
         JNIEnv* env = jniEnv();
@@ -642,16 +653,16 @@
 
         sp<PointerController> controller = mLocked.pointerController.promote();
         if (controller != NULL) {
-            updateInactivityFadeDelayLocked(controller);
+            updateInactivityTimeoutLocked(controller);
         }
     }
 }
 
-void NativeInputManager::updateInactivityFadeDelayLocked(const sp<PointerController>& controller) {
+void NativeInputManager::updateInactivityTimeoutLocked(const sp<PointerController>& controller) {
     bool lightsOut = mLocked.systemUiVisibility & ASYSTEM_UI_VISIBILITY_STATUS_BAR_HIDDEN;
-    controller->setInactivityFadeDelay(lightsOut
-            ? PointerController::INACTIVITY_FADE_DELAY_SHORT
-            : PointerController::INACTIVITY_FADE_DELAY_NORMAL);
+    controller->setInactivityTimeout(lightsOut
+            ? PointerController::INACTIVITY_TIMEOUT_SHORT
+            : PointerController::INACTIVITY_TIMEOUT_NORMAL);
 }
 
 bool NativeInputManager::isScreenOn() {
@@ -884,6 +895,17 @@
     return result;
 }
 
+void NativeInputManager::loadPointerResources(PointerResources* outResources) {
+    JNIEnv* env = jniEnv();
+
+    loadSystemIconAsSprite(env, mContextObj, POINTER_ICON_STYLE_SPOT_HOVER,
+            &outResources->spotHover);
+    loadSystemIconAsSprite(env, mContextObj, POINTER_ICON_STYLE_SPOT_TOUCH,
+            &outResources->spotTouch);
+    loadSystemIconAsSprite(env, mContextObj, POINTER_ICON_STYLE_SPOT_ANCHOR,
+            &outResources->spotAnchor);
+}
+
 
 // ----------------------------------------------------------------------------
 
@@ -899,10 +921,10 @@
 }
 
 static void android_server_InputManager_nativeInit(JNIEnv* env, jclass clazz,
-        jobject callbacks, jobject messageQueueObj) {
+        jobject contextObj, jobject callbacksObj, jobject messageQueueObj) {
     if (gNativeInputManager == NULL) {
         sp<Looper> looper = android_os_MessageQueue_getLooper(env, messageQueueObj);
-        gNativeInputManager = new NativeInputManager(callbacks, looper);
+        gNativeInputManager = new NativeInputManager(contextObj, callbacksObj, looper);
     } else {
         LOGE("Input manager already initialized.");
         jniThrowRuntimeException(env, "Input manager already initialized.");
@@ -1246,7 +1268,8 @@
 
 static JNINativeMethod gInputManagerMethods[] = {
     /* name, signature, funcPtr */
-    { "nativeInit", "(Lcom/android/server/wm/InputManager$Callbacks;Landroid/os/MessageQueue;)V",
+    { "nativeInit", "(Landroid/content/Context;"
+            "Lcom/android/server/wm/InputManager$Callbacks;Landroid/os/MessageQueue;)V",
             (void*) android_server_InputManager_nativeInit },
     { "nativeStart", "()V",
             (void*) android_server_InputManager_nativeStart },
@@ -1372,7 +1395,7 @@
             "getPointerLayer", "()I");
 
     GET_METHOD_ID(gCallbacksClassInfo.getPointerIcon, clazz,
-            "getPointerIcon", "()Lcom/android/server/wm/InputManager$PointerIcon;");
+            "getPointerIcon", "()Landroid/view/PointerIcon;");
 
     // KeyEvent
 
@@ -1421,19 +1444,6 @@
     GET_FIELD_ID(gConfigurationClassInfo.navigation, clazz,
             "navigation", "I");
 
-    // PointerIcon
-
-    FIND_CLASS(clazz, "com/android/server/wm/InputManager$PointerIcon");
-
-    GET_FIELD_ID(gPointerIconClassInfo.bitmap, clazz,
-            "bitmap", "Landroid/graphics/Bitmap;");
-
-    GET_FIELD_ID(gPointerIconClassInfo.hotSpotX, clazz,
-            "hotSpotX", "F");
-
-    GET_FIELD_ID(gPointerIconClassInfo.hotSpotY, clazz,
-            "hotSpotY", "F");
-
     return 0;
 }