Merge "Do not bother to remove pending installs" into jb-dev
diff --git a/api/16.txt b/api/16.txt
index 6aab939..7e9794f 100644
--- a/api/16.txt
+++ b/api/16.txt
@@ -22737,6 +22737,17 @@
method public void onPrepareSubMenu(android.view.SubMenu);
}
+ public final class Choreographer {
+ method public static android.view.Choreographer getInstance();
+ method public void postFrameCallback(android.view.Choreographer.FrameCallback);
+ method public void postFrameCallbackDelayed(android.view.Choreographer.FrameCallback, long);
+ method public void removeFrameCallback(android.view.Choreographer.FrameCallback);
+ }
+
+ public static abstract interface Choreographer.FrameCallback {
+ method public abstract void doFrame(long);
+ }
+
public abstract interface CollapsibleActionView {
method public abstract void onActionViewCollapsed();
method public abstract void onActionViewExpanded();
diff --git a/api/current.txt b/api/current.txt
index 6aab939..7e9794f 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -22737,6 +22737,17 @@
method public void onPrepareSubMenu(android.view.SubMenu);
}
+ public final class Choreographer {
+ method public static android.view.Choreographer getInstance();
+ method public void postFrameCallback(android.view.Choreographer.FrameCallback);
+ method public void postFrameCallbackDelayed(android.view.Choreographer.FrameCallback, long);
+ method public void removeFrameCallback(android.view.Choreographer.FrameCallback);
+ }
+
+ public static abstract interface Choreographer.FrameCallback {
+ method public abstract void doFrame(long);
+ }
+
public abstract interface CollapsibleActionView {
method public abstract void onActionViewCollapsed();
method public abstract void onActionViewExpanded();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index c7afd2b2..618f1f8 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.net.Uri;
@@ -31,6 +32,8 @@
import android.text.TextUtils;
import android.util.IntProperty;
import android.util.Log;
+import android.util.Slog;
+import android.util.TypedValue;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.RemoteViews;
@@ -1378,8 +1381,8 @@
private RemoteViews applyStandardTemplate(int resId) {
RemoteViews contentView = new RemoteViews(mContext.getPackageName(), resId);
- boolean hasLine3 = false;
- boolean hasLine2 = false;
+ boolean showLine3 = false;
+ boolean showLine2 = false;
int smallIconImageViewId = R.id.icon;
if (mLargeIcon != null) {
contentView.setImageViewBitmap(R.id.icon, mLargeIcon);
@@ -1401,15 +1404,13 @@
contentView.setTextViewText(R.id.title, mContentTitle);
}
if (mContentText != null) {
- contentView.setTextViewText(
- (mSubText != null) ? R.id.text2 : R.id.text,
- mContentText);
- hasLine3 = true;
+ contentView.setTextViewText(R.id.text, mContentText);
+ showLine3 = true;
}
if (mContentInfo != null) {
contentView.setTextViewText(R.id.info, mContentInfo);
contentView.setViewVisibility(R.id.info, View.VISIBLE);
- hasLine3 = true;
+ showLine3 = true;
} else if (mNumber > 0) {
final int tooBig = mContext.getResources().getInteger(
R.integer.status_bar_notification_info_maxnum);
@@ -1421,25 +1422,42 @@
contentView.setTextViewText(R.id.info, f.format(mNumber));
}
contentView.setViewVisibility(R.id.info, View.VISIBLE);
- hasLine3 = true;
+ showLine3 = true;
} else {
contentView.setViewVisibility(R.id.info, View.GONE);
}
+ // Need to show three lines?
if (mSubText != null) {
contentView.setTextViewText(R.id.text, mSubText);
- contentView.setViewVisibility(R.id.text2,
- mContentText != null ? View.VISIBLE : View.GONE);
+ if (mContentText != null) {
+ contentView.setTextViewText(R.id.text2, mContentText);
+ // need to shrink all the type to make sure everything fits
+ contentView.setViewVisibility(R.id.text2, View.VISIBLE);
+ showLine2 = true;
+ } else {
+ contentView.setViewVisibility(R.id.text2, View.GONE);
+ }
} else {
contentView.setViewVisibility(R.id.text2, View.GONE);
if (mProgressMax != 0 || mProgressIndeterminate) {
contentView.setProgressBar(
R.id.progress, mProgressMax, mProgress, mProgressIndeterminate);
contentView.setViewVisibility(R.id.progress, View.VISIBLE);
+ showLine2 = true;
} else {
contentView.setViewVisibility(R.id.progress, View.GONE);
}
}
+ if (showLine2) {
+ final Resources res = mContext.getResources();
+ final float subTextSize = res.getDimensionPixelSize(
+ R.dimen.notification_subtext_size);
+ contentView.setTextViewTextSize(R.id.text, TypedValue.COMPLEX_UNIT_PX, subTextSize);
+ // vertical centering
+ contentView.setViewPadding(R.id.line1, 0, 0, 0, 0);
+ }
+
if (mWhen != 0) {
if (mUseChronometer) {
contentView.setViewVisibility(R.id.chronometer, View.VISIBLE);
@@ -1451,7 +1469,7 @@
contentView.setLong(R.id.time, "setTime", mWhen);
}
}
- contentView.setViewVisibility(R.id.line3, hasLine3 ? View.VISIBLE : View.GONE);
+ contentView.setViewVisibility(R.id.line3, showLine3 ? View.VISIBLE : View.GONE);
return contentView;
}
@@ -1629,18 +1647,8 @@
mBuilder.setContentTitle(mBigContentTitle);
}
- if (mBuilder.mSubText == null) {
- mBuilder.setContentText(null);
- }
-
RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(layoutId);
- if (mBuilder.mSubText == null) {
- contentView.setViewVisibility(R.id.line3, View.GONE);
- } else {
- contentView.setViewVisibility(R.id.line3, View.VISIBLE);
- }
-
if (mBigContentTitle != null && mBigContentTitle.equals("")) {
contentView.setViewVisibility(R.id.line1, View.GONE);
} else {
@@ -1650,8 +1658,10 @@
if (mSummaryText != null && !mSummaryText.equals("")) {
contentView.setViewVisibility(R.id.overflow_title, View.VISIBLE);
contentView.setTextViewText(R.id.overflow_title, mSummaryText);
+ contentView.setViewVisibility(R.id.line3, View.GONE);
} else {
contentView.setViewVisibility(R.id.overflow_title, View.GONE);
+ contentView.setViewVisibility(R.id.line3, View.VISIBLE);
}
return contentView;
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 183cb88..aaa081c 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -24,16 +24,46 @@
import android.util.Log;
/**
- * Coordinates animations and drawing for UI on a particular thread.
- *
- * This object is thread-safe. Other threads can post callbacks to run at a later time
- * on the UI thread.
- *
- * Ensuring thread-safety is a little tricky because the {@link DisplayEventReceiver}
- * can only be accessed from the UI thread so operations that touch the event receiver
- * are posted to the UI thread if needed.
- *
- * @hide
+ * Coordinates the timing of animations, input and drawing.
+ * <p>
+ * The choreographer receives timing pulses (such as vertical synchronization)
+ * from the display subsystem then schedules work to occur as part of rendering
+ * the next display frame.
+ * </p><p>
+ * Applications typically interact with the choreographer indirectly using
+ * higher level abstractions in the animation framework or the view hierarchy.
+ * Here are some examples of things you can do using the higher-level APIs.
+ * </p>
+ * <ul>
+ * <li>To post an animation to be processed on a regular time basis synchronized with
+ * display frame rendering, use {@link android.animation.ValueAnimator#start}.</li>
+ * <li>To post a {@link Runnable} to be invoked once at the beginning of the next display
+ * frame, use {@link View#postOnAnimation}.</li>
+ * <li>To post a {@link Runnable} to be invoked once at the beginning of the next display
+ * frame after a delay, use {@link View#postOnAnimationDelayed}.</li>
+ * <li>To post a call to {@link View#invalidate()} to occur once at the beginning of the
+ * next display frame, use {@link View#postInvalidateOnAnimation()} or
+ * {@link View#postInvalidateOnAnimation(int, int, int, int)}.</li>
+ * <li>To ensure that the contents of a {@link View} scroll smoothly and are drawn in
+ * sync with display frame rendering, do nothing. This already happens automatically.
+ * {@link View#onDraw} will be called at the appropriate time.</li>
+ * </ul>
+ * <p>
+ * However, there are a few cases where you might want to use the functions of the
+ * choreographer directly in your application. Here are some examples.
+ * </p>
+ * <ul>
+ * <li>If your application does its rendering in a different thread, possibly using GL,
+ * or does not use the animation framework or view hierarchy at all
+ * and you want to ensure that it is appropriately synchronized with the display, then use
+ * {@link Choreographer#postFrameCallback}.</li>
+ * <li>... and that's about it.</li>
+ * </ul>
+ * <p>
+ * Each {@link Looper} thread has its own choreographer. Other threads can
+ * post callbacks to run on the choreographer but they will run on the {@link Looper}
+ * to which the choreographer belongs.
+ * </p>
*/
public final class Choreographer {
private static final String TAG = "Choreographer";
@@ -79,13 +109,22 @@
private static final int MSG_DO_SCHEDULE_VSYNC = 1;
private static final int MSG_DO_SCHEDULE_CALLBACK = 2;
+ // All frame callbacks posted by applications have this token.
+ private static final Object FRAME_CALLBACK_TOKEN = new Object() {
+ public String toString() { return "FRAME_CALLBACK_TOKEN"; }
+ };
+
private final Object mLock = new Object();
private final Looper mLooper;
private final FrameHandler mHandler;
+
+ // The display event receiver can only be accessed by the looper thread to which
+ // it is attached. We take care to ensure that we post message to the looper
+ // if appropriate when interacting with the display event receiver.
private final FrameDisplayEventReceiver mDisplayEventReceiver;
- private Callback mCallbackPool;
+ private CallbackRecord mCallbackPool;
private final CallbackQueue[] mCallbackQueues;
@@ -96,17 +135,20 @@
/**
* Callback type: Input callback. Runs first.
+ * @hide
*/
public static final int CALLBACK_INPUT = 0;
/**
* Callback type: Animation callback. Runs before traversals.
+ * @hide
*/
public static final int CALLBACK_ANIMATION = 1;
/**
* Callback type: Traversal callback. Handles layout and draw. Runs last
* after all other asynchronous messages have been handled.
+ * @hide
*/
public static final int CALLBACK_TRAVERSAL = 2;
@@ -138,32 +180,38 @@
}
/**
- * The amount of time, in milliseconds, between each frame of the animation. This is a
- * requested time that the animation will attempt to honor, but the actual delay between
- * frames may be different, depending on system load and capabilities. This is a static
+ * The amount of time, in milliseconds, between each frame of the animation.
+ * <p>
+ * This is a requested time that the animation will attempt to honor, but the actual delay
+ * between frames may be different, depending on system load and capabilities. This is a static
* function because the same delay will be applied to all animations, since they are all
* run off of a single timing loop.
- *
+ * </p><p>
* The frame delay may be ignored when the animation system uses an external timing
* source, such as the display refresh rate (vsync), to govern animations.
+ * </p>
*
* @return the requested time between frames, in milliseconds
+ * @hide
*/
public static long getFrameDelay() {
return sFrameDelay;
}
/**
- * The amount of time, in milliseconds, between each frame of the animation. This is a
- * requested time that the animation will attempt to honor, but the actual delay between
- * frames may be different, depending on system load and capabilities. This is a static
+ * The amount of time, in milliseconds, between each frame of the animation.
+ * <p>
+ * This is a requested time that the animation will attempt to honor, but the actual delay
+ * between frames may be different, depending on system load and capabilities. This is a static
* function because the same delay will be applied to all animations, since they are all
* run off of a single timing loop.
- *
+ * </p><p>
* The frame delay may be ignored when the animation system uses an external timing
* source, such as the display refresh rate (vsync), to govern animations.
+ * </p>
*
* @param frameDelay the requested time between frames, in milliseconds
+ * @hide
*/
public static void setFrameDelay(long frameDelay) {
sFrameDelay = frameDelay;
@@ -171,23 +219,25 @@
/**
* Subtracts typical frame delay time from a delay interval in milliseconds.
- *
+ * <p>
* This method can be used to compensate for animation delay times that have baked
* in assumptions about the frame delay. For example, it's quite common for code to
* assume a 60Hz frame time and bake in a 16ms delay. When we call
* {@link #postAnimationCallbackDelayed} we want to know how long to wait before
* posting the animation callback but let the animation timer take care of the remaining
* frame delay time.
- *
+ * </p><p>
* This method is somewhat conservative about how much of the frame delay it
* subtracts. It uses the same value returned by {@link #getFrameDelay} which by
* default is 10ms even though many parts of the system assume 16ms. Consequently,
* we might still wait 6ms before posting an animation callback that we want to run
* on the next frame, but this is much better than waiting a whole 16ms and likely
* missing the deadline.
+ * </p>
*
* @param delayMillis The original delay time including an assumed frame delay.
* @return The adjusted delay time with the assumed frame delay subtracted out.
+ * @hide
*/
public static long subtractFrameDelay(long delayMillis) {
final long frameDelay = sFrameDelay;
@@ -196,21 +246,26 @@
/**
* Posts a callback to run on the next frame.
- * The callback only runs once and then is automatically removed.
+ * <p>
+ * The callback runs once then is automatically removed.
+ * </p>
*
* @param callbackType The callback type.
* @param action The callback action to run during the next frame.
* @param token The callback token, or null if none.
*
* @see #removeCallbacks
+ * @hide
*/
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
/**
- * Posts a callback to run on the next frame following the specified delay.
- * The callback only runs once and then is automatically removed.
+ * Posts a callback to run on the next frame after the specified delay.
+ * <p>
+ * The callback runs once then is automatically removed.
+ * </p>
*
* @param callbackType The callback type.
* @param action The callback action to run during the next frame after the specified delay.
@@ -218,6 +273,7 @@
* @param delayMillis The delay time in milliseconds.
*
* @see #removeCallback
+ * @hide
*/
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
@@ -228,6 +284,11 @@
throw new IllegalArgumentException("callbackType is invalid");
}
+ postCallbackDelayedInternal(callbackType, action, token, delayMillis);
+ }
+
+ private void postCallbackDelayedInternal(int callbackType,
+ Object action, Object token, long delayMillis) {
if (DEBUG) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
@@ -261,12 +322,17 @@
*
* @see #postCallback
* @see #postCallbackDelayed
+ * @hide
*/
public void removeCallbacks(int callbackType, Runnable action, Object token) {
if (callbackType < 0 || callbackType > CALLBACK_LAST) {
throw new IllegalArgumentException("callbackType is invalid");
}
+ removeCallbacksInternal(callbackType, action, token);
+ }
+
+ private void removeCallbacksInternal(int callbackType, Object action, Object token) {
if (DEBUG) {
Log.d(TAG, "RemoveCallbacks: type=" + callbackType
+ ", action=" + action + ", token=" + token);
@@ -281,17 +347,81 @@
}
/**
- * Gets the time when the current frame started. The frame time should be used
- * instead of {@link SystemClock#uptimeMillis()} to synchronize animations.
- * This helps to reduce inter-frame jitter because the frame time is fixed at the
- * time the frame was scheduled to start, regardless of when the animations or
- * drawing code actually ran.
+ * Posts a frame callback to run on the next frame.
+ * <p>
+ * The callback runs once then is automatically removed.
+ * </p>
*
+ * @param callback The frame callback to run during the next frame.
+ *
+ * @see #postFrameCallbackDelayed
+ * @see #removeFrameCallback
+ */
+ public void postFrameCallback(FrameCallback callback) {
+ postFrameCallbackDelayed(callback, 0);
+ }
+
+ /**
+ * Posts a frame callback to run on the next frame after the specified delay.
+ * <p>
+ * The callback runs once then is automatically removed.
+ * </p>
+ *
+ * @param callback The frame callback to run during the next frame.
+ * @param delayMillis The delay time in milliseconds.
+ *
+ * @see #postFrameCallback
+ * @see #removeFrameCallback
+ */
+ public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+
+ postCallbackDelayedInternal(CALLBACK_ANIMATION,
+ callback, FRAME_CALLBACK_TOKEN, delayMillis);
+ }
+
+ /**
+ * Removes a previously posted frame callback.
+ *
+ * @param callback The frame callback to remove.
+ *
+ * @see #postFrameCallback
+ * @see #postFrameCallbackDelayed
+ */
+ public void removeFrameCallback(FrameCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+
+ removeCallbacksInternal(CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN);
+ }
+
+ /**
+ * Gets the time when the current frame started.
+ * <p>
+ * This method provides the time in nanoseconds when the frame started being rendered.
+ * The frame time provides a stable time base for synchronizing animations
+ * and drawing. It should be used instead of {@link SystemClock#uptimeMillis()}
+ * or {@link System#nanoTime()} for animations and drawing in the UI. Using the frame
+ * time helps to reduce inter-frame jitter because the frame time is fixed at the time
+ * the frame was scheduled to start, regardless of when the animations or drawing
+ * callback actually runs. All callbacks that run as part of rendering a frame will
+ * observe the same frame time so using the frame time also helps to synchronize effects
+ * that are performed by different callbacks.
+ * </p><p>
+ * Please note that the framework already takes care to process animations and
+ * drawing using the frame time as a stable time base. Most applications should
+ * not need to use the frame time information directly.
+ * </p><p>
* This method should only be called from within a callback.
+ * </p>
*
* @return The frame start time, in the {@link SystemClock#uptimeMillis()} time base.
*
* @throws IllegalStateException if no frame is in progress.
+ * @hide
*/
public long getFrameTime() {
return getFrameTimeNanos() / NANOS_PER_MS;
@@ -303,6 +433,7 @@
* @return The frame start time, in the {@link System#nanoTime()} time base.
*
* @throws IllegalStateException if no frame is in progress.
+ * @hide
*/
public long getFrameTimeNanos() {
synchronized (mLock) {
@@ -345,7 +476,7 @@
}
}
- void doFrame(long timestampNanos, int frame) {
+ void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
@@ -353,7 +484,7 @@
}
startNanos = System.nanoTime();
- final long jitterNanos = startNanos - timestampNanos;
+ final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
if (DEBUG) {
@@ -363,10 +494,10 @@
+ "Setting frame time to " + (lastFrameOffset * 0.000001f)
+ " ms in the past.");
}
- timestampNanos = startNanos - lastFrameOffset;
+ frameTimeNanos = startNanos - lastFrameOffset;
}
- if (timestampNanos < mLastFrameTimeNanos) {
+ if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ "previously skipped frame. Waiting for next vsync");
@@ -376,24 +507,27 @@
}
mFrameScheduled = false;
- mLastFrameTimeNanos = timestampNanos;
+ mLastFrameTimeNanos = frameTimeNanos;
}
- doCallbacks(Choreographer.CALLBACK_INPUT);
- doCallbacks(Choreographer.CALLBACK_ANIMATION);
- doCallbacks(Choreographer.CALLBACK_TRAVERSAL);
+ doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
+ doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
+ doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
if (DEBUG) {
final long endNanos = System.nanoTime();
Log.d(TAG, "Frame " + frame + ": Finished, took "
+ (endNanos - startNanos) * 0.000001f + " ms, latency "
- + (startNanos - timestampNanos) * 0.000001f + " ms.");
+ + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
}
}
- void doCallbacks(int callbackType) {
- Callback callbacks;
+ void doCallbacks(int callbackType, long frameTimeNanos) {
+ CallbackRecord callbacks;
synchronized (mLock) {
+ // We use "now" to determine when callbacks become due because it's possible
+ // for earlier processing phases in a frame to post callbacks that should run
+ // in a following phase, such as an input event that causes an animation to start.
final long now = SystemClock.uptimeMillis();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now);
if (callbacks == null) {
@@ -402,19 +536,19 @@
mCallbacksRunning = true;
}
try {
- for (Callback c = callbacks; c != null; c = c.next) {
+ for (CallbackRecord c = callbacks; c != null; c = c.next) {
if (DEBUG) {
Log.d(TAG, "RunCallback: type=" + callbackType
+ ", action=" + c.action + ", token=" + c.token
+ ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
}
- c.action.run();
+ c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
- final Callback next = callbacks.next;
+ final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
@@ -449,10 +583,10 @@
return Looper.myLooper() == mLooper;
}
- private Callback obtainCallbackLocked(long dueTime, Runnable action, Object token) {
- Callback callback = mCallbackPool;
+ private CallbackRecord obtainCallbackLocked(long dueTime, Object action, Object token) {
+ CallbackRecord callback = mCallbackPool;
if (callback == null) {
- callback = new Callback();
+ callback = new CallbackRecord();
} else {
mCallbackPool = callback.next;
callback.next = null;
@@ -463,13 +597,44 @@
return callback;
}
- private void recycleCallbackLocked(Callback callback) {
+ private void recycleCallbackLocked(CallbackRecord callback) {
callback.action = null;
callback.token = null;
callback.next = mCallbackPool;
mCallbackPool = callback;
}
+ /**
+ * Implement this interface to receive a callback when a new display frame is
+ * being rendered. The callback is invoked on the {@link Looper} thread to
+ * which the {@link Choreographer} is attached.
+ */
+ public interface FrameCallback {
+ /**
+ * Called when a new display frame is being rendered.
+ * <p>
+ * This method provides the time in nanoseconds when the frame started being rendered.
+ * The frame time provides a stable time base for synchronizing animations
+ * and drawing. It should be used instead of {@link SystemClock#uptimeMillis()}
+ * or {@link System#nanoTime()} for animations and drawing in the UI. Using the frame
+ * time helps to reduce inter-frame jitter because the frame time is fixed at the time
+ * the frame was scheduled to start, regardless of when the animations or drawing
+ * callback actually runs. All callbacks that run as part of rendering a frame will
+ * observe the same frame time so using the frame time also helps to synchronize effects
+ * that are performed by different callbacks.
+ * </p><p>
+ * Please note that the framework already takes care to process animations and
+ * drawing using the frame time as a stable time base. Most applications should
+ * not need to use the frame time information directly.
+ * </p>
+ *
+ * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
+ * in the {@link System#nanoTime()} timebase. Divide this value by {@code 1000000}
+ * to convert it to the {@link SystemClock#uptimeMillis()} time base.
+ */
+ public void doFrame(long frameTimeNanos);
+ }
+
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
@@ -520,28 +685,36 @@
}
}
- private static final class Callback {
- public Callback next;
+ private static final class CallbackRecord {
+ public CallbackRecord next;
public long dueTime;
- public Runnable action;
+ public Object action; // Runnable or FrameCallback
public Object token;
+
+ public void run(long frameTimeNanos) {
+ if (token == FRAME_CALLBACK_TOKEN) {
+ ((FrameCallback)action).doFrame(frameTimeNanos);
+ } else {
+ ((Runnable)action).run();
+ }
+ }
}
private final class CallbackQueue {
- private Callback mHead;
+ private CallbackRecord mHead;
public boolean hasDueCallbacksLocked(long now) {
return mHead != null && mHead.dueTime <= now;
}
- public Callback extractDueCallbacksLocked(long now) {
- Callback callbacks = mHead;
+ public CallbackRecord extractDueCallbacksLocked(long now) {
+ CallbackRecord callbacks = mHead;
if (callbacks == null || callbacks.dueTime > now) {
return null;
}
- Callback last = callbacks;
- Callback next = last.next;
+ CallbackRecord last = callbacks;
+ CallbackRecord next = last.next;
while (next != null) {
if (next.dueTime > now) {
last.next = null;
@@ -554,9 +727,9 @@
return callbacks;
}
- public void addCallbackLocked(long dueTime, Runnable action, Object token) {
- Callback callback = obtainCallbackLocked(dueTime, action, token);
- Callback entry = mHead;
+ public void addCallbackLocked(long dueTime, Object action, Object token) {
+ CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
+ CallbackRecord entry = mHead;
if (entry == null) {
mHead = callback;
return;
@@ -576,10 +749,10 @@
entry.next = callback;
}
- public void removeCallbacksLocked(Runnable action, Object token) {
- Callback predecessor = null;
- for (Callback callback = mHead; callback != null;) {
- final Callback next = callback.next;
+ public void removeCallbacksLocked(Object action, Object token) {
+ CallbackRecord predecessor = null;
+ for (CallbackRecord callback = mHead; callback != null;) {
+ final CallbackRecord next = callback.next;
if ((action == null || callback.action == action)
&& (token == null || callback.token == token)) {
if (predecessor != null) {
diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java
index 411aed3..a719a01 100644
--- a/core/java/android/view/TextureView.java
+++ b/core/java/android/view/TextureView.java
@@ -374,6 +374,14 @@
// tell mLayer about it and set the SurfaceTexture to use the
// current view size.
mUpdateSurface = false;
+
+ // Since we are updating the layer, force an update to ensure its
+ // parameters are correct (width, height, transform, etc.)
+ synchronized (mLock) {
+ mUpdateLayer = true;
+ }
+ mMatrixChanged = true;
+
mAttachInfo.mHardwareRenderer.setSurfaceTexture(mLayer, mSurface);
nSetDefaultBufferSize(mSurface, getWidth(), getHeight());
}
@@ -471,7 +479,7 @@
}
private void applyTransformMatrix() {
- if (mMatrixChanged) {
+ if (mMatrixChanged && mLayer != null) {
mLayer.setTransform(mMatrix);
mMatrixChanged = false;
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index b69450c..832d575 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -6212,15 +6212,6 @@
// Clear the text navigation state.
setAccessibilityCursorPosition(-1);
-
- // Try to move accessibility focus to the input focus.
- View rootView = getRootView();
- if (rootView != null) {
- View inputFocus = rootView.findFocus();
- if (inputFocus != null) {
- inputFocus.requestAccessibilityFocus();
- }
- }
}
}
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 9134966..823befb 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -193,19 +193,12 @@
private static final int MAXIMUM_FLING_VELOCITY = 8000;
/**
- * Distance in dips between a touch up event denoting the end of a touch exploration
- * gesture and the touch up event of a subsequent tap for the latter tap to be
- * considered as a tap i.e. to perform a click.
- */
- private static final int TOUCH_EXPLORE_TAP_SLOP = 80;
-
- /**
* Delay before dispatching a recurring accessibility event in milliseconds.
* This delay guarantees that a recurring event will be send at most once
* during the {@link #SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS} time
* frame.
*/
- private static final long SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS = 400;
+ private static final long SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS = 100;
/**
* The maximum size of View's drawing cache, expressed in bytes. This size
@@ -238,7 +231,6 @@
private final int mDoubleTapTouchSlop;
private final int mPagingTouchSlop;
private final int mDoubleTapSlop;
- private final int mScaledTouchExploreTapSlop;
private final int mWindowTouchSlop;
private final int mMaximumDrawingCacheSize;
private final int mOverscrollDistance;
@@ -265,7 +257,6 @@
mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP;
mPagingTouchSlop = PAGING_TOUCH_SLOP;
mDoubleTapSlop = DOUBLE_TAP_SLOP;
- mScaledTouchExploreTapSlop = TOUCH_EXPLORE_TAP_SLOP;
mWindowTouchSlop = WINDOW_TOUCH_SLOP;
//noinspection deprecation
mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE;
@@ -302,7 +293,6 @@
mMaximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f);
mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f);
mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
- mScaledTouchExploreTapSlop = (int) (density * TOUCH_EXPLORE_TAP_SLOP + 0.5f);
mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);
final Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
@@ -553,17 +543,6 @@
}
/**
- * @return Distance in pixels between a touch up event denoting the end of a touch exploration
- * gesture and the touch up event of a subsequent tap for the latter tap to be
- * considered as a tap i.e. to perform a click.
- *
- * @hide
- */
- public int getScaledTouchExploreTapSlop() {
- return mScaledTouchExploreTapSlop;
- }
-
- /**
* Interval for dispatching a recurring accessibility event in milliseconds.
* This interval guarantees that a recurring event will be send at most once
* during the {@link #getSendRecurringAccessibilityEventsInterval()} time frame.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 512507b..c9a41ad 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2895,20 +2895,6 @@
if (hasWindowFocus) {
mView.sendAccessibilityEvent(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
- // Give accessibility focus to the view that has input
- // focus if such, otherwise to the first one.
- if (mView instanceof ViewGroup) {
- ViewGroup viewGroup = (ViewGroup) mView;
- View focused = viewGroup.findFocus();
- if (focused != null) {
- focused.requestAccessibilityFocus();
- }
- }
- // There is no accessibility focus, despite our effort
- // above, now just give it to the first view.
- if (mAccessibilityFocusedHost == null) {
- mView.requestAccessibilityFocus();
- }
}
}
}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 42c4ed6..6cee0f3 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -1456,7 +1456,7 @@
final int lastVisiblePosition = getLastVisiblePosition();
if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
&& mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
- return;
+ return;
} else {
mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
@@ -2278,28 +2278,37 @@
super.onInitializeAccessibilityNodeInfo(host, info);
final int position = getPositionForView(host);
+ final ListAdapter adapter = getAdapter();
- if (position == INVALID_POSITION) {
+ if ((position == INVALID_POSITION) || (adapter == null)) {
+ // Cannot perform actions on invalid items.
+ info.setEnabled(false);
return;
}
- if (isClickable() && isEnabled()) {
- info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
- info.setClickable(true);
- }
-
- if (isLongClickable() && isEnabled()) {
- info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
- info.setLongClickable(true);
- }
-
- if (isEnabled()) {
- info.addAction(AccessibilityNodeInfo.ACTION_SELECT);
+ if (!isEnabled() || !adapter.isEnabled(position)) {
+ // Cannot perform actions on invalid items.
+ info.setEnabled(false);
+ return;
}
if (position == getSelectedItemPosition()) {
info.setSelected(true);
+ info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
+ } else {
+ info.addAction(AccessibilityNodeInfo.ACTION_SELECT);
}
+
+ if (isClickable()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+ info.setClickable(true);
+ }
+
+ if (isLongClickable()) {
+ info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+ info.setLongClickable(true);
+ }
+
}
@Override
@@ -2309,22 +2318,33 @@
}
final int position = getPositionForView(host);
+ final ListAdapter adapter = getAdapter();
- if (position == INVALID_POSITION) {
+ if ((position == INVALID_POSITION) || (adapter == null)) {
+ // Cannot perform actions on invalid items.
return false;
}
- if (!isEnabled()) {
+ if (!isEnabled() || !adapter.isEnabled(position)) {
+ // Cannot perform actions on disabled items.
return false;
}
final long id = getItemIdAtPosition(position);
switch (action) {
+ case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: {
+ if (getSelectedItemPosition() == position) {
+ setSelection(INVALID_POSITION);
+ return true;
+ }
+ } return false;
case AccessibilityNodeInfo.ACTION_SELECT: {
- setSelection(position);
- return true;
- }
+ if (getSelectedItemPosition() != position) {
+ setSelection(position);
+ return true;
+ }
+ } return false;
case AccessibilityNodeInfo.ACTION_CLICK: {
if (isClickable()) {
return performItemClick(host, position, id);
diff --git a/core/java/com/android/internal/widget/multiwaveview/MultiWaveView.java b/core/java/com/android/internal/widget/multiwaveview/MultiWaveView.java
index 6136206..d6fb847 100644
--- a/core/java/com/android/internal/widget/multiwaveview/MultiWaveView.java
+++ b/core/java/com/android/internal/widget/multiwaveview/MultiWaveView.java
@@ -67,6 +67,7 @@
public void onReleased(View v, int handle);
public void onTrigger(View v, int target);
public void onGrabbedStateChange(View v, int handle);
+ public void onFinishFinalAnimation();
}
// Tuneable parameters for animation
@@ -77,9 +78,13 @@
private static final int HIDE_ANIMATION_DELAY = 200;
private static final int HIDE_ANIMATION_DURATION = 200;
private static final int SHOW_ANIMATION_DURATION = 200;
- private static final int SHOW_ANIMATION_DELAY = 0;
+ private static final int SHOW_ANIMATION_DELAY = 50;
private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f;
- private static final float TARGET_INITIAL_POSITION_SCALE = 0.8f;
+ private static final float TARGET_SCALE_SELECTED = 0.8f;
+ private static final long INITIAL_SHOW_HANDLE_DURATION = 200;
+ private static final float TARGET_SCALE_UNSELECTED = 1.0f;
+ private static final float RING_SCALE_UNSELECTED = 0.5f;
+ private static final float RING_SCALE_SELECTED = 1.5f;
private TimeInterpolator mChevronAnimationInterpolator = Ease.Quad.easeOut;
@@ -126,6 +131,15 @@
}
}
+ public void cancel() {
+ final int count = size();
+ for (int i = 0; i < count; i++) {
+ Tweener anim = get(i);
+ anim.animator.cancel();
+ }
+ clear();
+ }
+
public void stop() {
final int count = size();
for (int i = 0; i < count; i++) {
@@ -143,6 +157,7 @@
private AnimatorListener mResetListener = new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animator) {
switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
+ dispatchOnFinishFinalAnimation();
}
};
@@ -150,6 +165,7 @@
public void onAnimationEnd(Animator animator) {
ping();
switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
+ dispatchOnFinishFinalAnimation();
}
};
@@ -349,7 +365,7 @@
stopHandleAnimation();
deactivateTargets();
showTargets(true);
- mHandleDrawable.setState(TargetDrawable.STATE_ACTIVE);
+ activateHandle();
setGrabbedState(OnTriggerListener.CENTER_HANDLE);
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
announceTargets();
@@ -368,6 +384,19 @@
}
}
+ private void activateHandle() {
+ mHandleDrawable.setState(TargetDrawable.STATE_ACTIVE);
+ if (mAlwaysTrackFinger) {
+ mHandleAnimations.stop();
+ mHandleDrawable.setAlpha(0.0f);
+ mHandleAnimations.add(Tweener.to(mHandleDrawable, INITIAL_SHOW_HANDLE_DURATION,
+ "ease", Ease.Cubic.easeIn,
+ "alpha", 1.0f,
+ "onUpdate", mUpdateListener));
+ mHandleAnimations.start();
+ }
+ }
+
/**
* Animation used to attract user's attention to the target button.
* Assumes mChevronDrawables is an a list with an even number of chevrons filled with
@@ -463,6 +492,12 @@
}
}
+ private void dispatchOnFinishFinalAnimation() {
+ if (mOnTriggerListener != null) {
+ mOnTriggerListener.onFinishFinalAnimation();
+ }
+ }
+
private void doFinish() {
final int activeTarget = mActiveTarget;
boolean targetHit = activeTarget != -1;
@@ -471,8 +506,9 @@
hideTargets(true);
// Highlight the selected one
- mHandleDrawable.setAlpha(targetHit ? 0.0f : 1.0f);
+ mHandleAnimations.cancel();
if (targetHit) {
+ mHandleDrawable.setAlpha(0.0f);
mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);
hideUnselected(activeTarget);
@@ -483,12 +519,11 @@
// Animate handle back to the center based on current state.
int delay = targetHit ? RETURN_TO_HOME_DELAY : 0;
- int duration = targetHit ? 0 : RETURN_TO_HOME_DURATION;
- mHandleAnimations.stop();
+ int duration = RETURN_TO_HOME_DURATION;
mHandleAnimations.add(Tweener.to(mHandleDrawable, duration,
"ease", Ease.Quart.easeOut,
"delay", delay,
- "alpha", 1.0f,
+ "alpha", mAlwaysTrackFinger ? 0.0f : 1.0f,
"x", 0,
"y", 0,
"onUpdate", mUpdateListener,
@@ -508,12 +543,15 @@
}
private void hideTargets(boolean animate) {
- mTargetAnimations.stop();
+ mTargetAnimations.cancel();
// Note: these animations should complete at the same time so that we can swap out
// the target assets asynchronously from the setTargetResources() call.
mAnimatingTargets = animate;
final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
final int delay = animate ? HIDE_ANIMATION_DELAY : 0;
+ final boolean targetSelected = mActiveTarget != -1;
+
+ final float targetScale = targetSelected ? TARGET_SCALE_SELECTED : TARGET_SCALE_UNSELECTED;
final int length = mTargetDrawables.size();
for (int i = 0; i < length; i++) {
TargetDrawable target = mTargetDrawables.get(i);
@@ -521,13 +559,13 @@
mTargetAnimations.add(Tweener.to(target, duration,
"ease", Ease.Cubic.easeOut,
"alpha", 0.0f,
- "scaleX", TARGET_INITIAL_POSITION_SCALE,
- "scaleY", TARGET_INITIAL_POSITION_SCALE,
+ "scaleX", targetScale,
+ "scaleY", targetScale,
"delay", delay,
"onUpdate", mUpdateListener));
}
- float ringScaleTarget = mActiveTarget != -1 ? 1.5f : 0.5f;
+ final float ringScaleTarget = targetSelected ? RING_SCALE_SELECTED : RING_SCALE_UNSELECTED;
mTargetAnimations.add(Tweener.to(mOuterRing, duration,
"ease", Ease.Cubic.easeOut,
"alpha", 0.0f,
@@ -544,13 +582,14 @@
mTargetAnimations.stop();
mAnimatingTargets = animate;
final int delay = animate ? SHOW_ANIMATION_DELAY : 0;
+ final int duration = animate ? SHOW_ANIMATION_DURATION : 0;
final int length = mTargetDrawables.size();
for (int i = 0; i < length; i++) {
TargetDrawable target = mTargetDrawables.get(i);
target.setState(TargetDrawable.STATE_INACTIVE);
- target.setScaleX(TARGET_INITIAL_POSITION_SCALE);
- target.setScaleY(TARGET_INITIAL_POSITION_SCALE);
- mTargetAnimations.add(Tweener.to(target, animate ? SHOW_ANIMATION_DURATION : 0,
+ target.setScaleX(TARGET_SCALE_SELECTED);
+ target.setScaleY(TARGET_SCALE_SELECTED);
+ mTargetAnimations.add(Tweener.to(target, duration,
"ease", Ease.Cubic.easeOut,
"alpha", 1.0f,
"scaleX", 1.0f,
@@ -558,9 +597,7 @@
"delay", delay,
"onUpdate", mUpdateListener));
}
- mOuterRing.setScaleX(0.5f);
- mOuterRing.setScaleY(0.5f);
- mTargetAnimations.add(Tweener.to(mOuterRing, animate ? SHOW_ANIMATION_DURATION : 0,
+ mTargetAnimations.add(Tweener.to(mOuterRing, duration,
"ease", Ease.Cubic.easeOut,
"alpha", 1.0f,
"scaleX", 1.0f,
@@ -572,10 +609,6 @@
mTargetAnimations.start();
}
- private void stopTargetAnimation() {
- mTargetAnimations.stop();
- }
-
private void vibrate() {
if (mVibrator != null) {
mVibrator.vibrate(mVibrationDuration);
@@ -708,7 +741,7 @@
public void reset(boolean animate) {
stopChevronAnimation();
stopHandleAnimation();
- stopTargetAnimation();
+ mTargetAnimations.stop();
hideChevrons();
hideTargets(animate);
mHandleDrawable.setX(0);
@@ -760,7 +793,7 @@
private void handleDown(MotionEvent event) {
if (!trySwitchToFirstTouchState(event.getX(), event.getY())) {
mDragging = false;
- stopTargetAnimation();
+ mTargetAnimations.cancel();
ping();
}
}
@@ -815,8 +848,8 @@
// For more than one target, snap to the closest one less than hitRadius away.
float best = Float.MAX_VALUE;
final float hitRadius2 = mHitRadius * mHitRadius;
+ // Find first target in range
for (int i = 0; i < ntargets; i++) {
- // Snap to the first target in range
TargetDrawable target = targets.get(i);
float dx = limitX - target.getX();
float dy = limitY - target.getY();
@@ -842,10 +875,15 @@
float newX = singleTarget ? x : target.getX();
float newY = singleTarget ? y : target.getY();
moveHandleTo(newX, newY, false);
+ mHandleAnimations.cancel();
+ mHandleDrawable.setAlpha(0.0f);
} else {
switchToState(STATE_TRACKING, x, y);
+ if (mActiveTarget != -1) {
+ mHandleAnimations.cancel();
+ mHandleDrawable.setAlpha(1.0f);
+ }
moveHandleTo(x, y, false);
- mHandleDrawable.setAlpha(1.0f);
}
// Draw handle outside parent's bounds
@@ -857,7 +895,6 @@
TargetDrawable target = targets.get(mActiveTarget);
if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
target.setState(TargetDrawable.STATE_INACTIVE);
- mHandleDrawable.setAlpha(1.0f);
}
}
// Focus the new target
@@ -865,7 +902,6 @@
TargetDrawable target = targets.get(activeTarget);
if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
target.setState(TargetDrawable.STATE_FOCUSED);
- mHandleDrawable.setAlpha(0.0f);
}
dispatchGrabbedEvent(activeTarget);
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
diff --git a/core/res/res/layout/notification_template_base.xml b/core/res/res/layout/notification_template_base.xml
index 3692a4d..ed680a9 100644
--- a/core/res/res/layout/notification_template_base.xml
+++ b/core/res/res/layout/notification_template_base.xml
@@ -38,14 +38,15 @@
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp"
- android:paddingTop="4dp"
- android:paddingBottom="4dp"
- android:gravity="center_vertical"
+ android:paddingTop="2dp"
+ android:paddingBottom="2dp"
+ android:gravity="top"
>
<LinearLayout
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:paddingTop="6dp"
android:orientation="horizontal"
>
<TextView android:id="@+id/title"
@@ -85,8 +86,15 @@
android:ellipsize="marquee"
android:visibility="gone"
/>
+ <ProgressBar
+ android:id="@android:id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="12dp"
+ android:visibility="gone"
+ style="?android:attr/progressBarStyleHorizontal"
+ />
<TextView android:id="@+id/overflow_title"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title"
+ android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
@@ -123,21 +131,14 @@
/>
<ImageView android:id="@+id/right_icon"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="0"
- android:scaleType="center"
+ android:scaleType="centerInside"
android:paddingLeft="8dp"
android:visibility="gone"
android:drawableAlpha="180"
/>
</LinearLayout>
- <ProgressBar
- android:id="@android:id/progress"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone"
- style="?android:attr/progressBarStyleHorizontal"
- />
</LinearLayout>
</FrameLayout>
diff --git a/core/res/res/layout/notification_template_big_base.xml b/core/res/res/layout/notification_template_big_base.xml
index d50b792..f2c204f 100644
--- a/core/res/res/layout/notification_template_big_base.xml
+++ b/core/res/res/layout/notification_template_big_base.xml
@@ -38,9 +38,9 @@
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp"
- android:paddingTop="4dp"
- android:paddingBottom="4dp"
- android:gravity="center_vertical"
+ android:paddingTop="2dp"
+ android:paddingBottom="2dp"
+ android:gravity="top"
>
<LinearLayout
android:layout_width="match_parent"
@@ -52,6 +52,7 @@
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:paddingTop="6dp"
android:orientation="horizontal"
>
<TextView android:id="@+id/title"
@@ -126,10 +127,10 @@
/>
<ImageView android:id="@+id/right_icon"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="0"
- android:scaleType="center"
+ android:scaleType="centerInside"
android:paddingLeft="8dp"
android:visibility="gone"
android:drawableAlpha="180"
@@ -138,7 +139,7 @@
<ProgressBar
android:id="@android:id/progress"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="12dp"
android:visibility="gone"
style="?android:attr/progressBarStyleHorizontal"
/>
diff --git a/core/res/res/layout/notification_template_big_picture.xml b/core/res/res/layout/notification_template_big_picture.xml
index f98970a..9b896a4 100644
--- a/core/res/res/layout/notification_template_big_picture.xml
+++ b/core/res/res/layout/notification_template_big_picture.xml
@@ -31,6 +31,13 @@
android:layout_gravity="bottom"
android:scaleType="centerCrop"
/>
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="6dp"
+ android:layout_marginTop="64dp"
+ android:scaleType="fitXY"
+ android:src="@drawable/title_bar_shadow"
+ />
<include layout="@layout/notification_template_base"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/core/res/res/layout/notification_template_big_text.xml b/core/res/res/layout/notification_template_big_text.xml
index 210bc13..304f2b5 100644
--- a/core/res/res/layout/notification_template_big_text.xml
+++ b/core/res/res/layout/notification_template_big_text.xml
@@ -36,9 +36,9 @@
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp"
- android:paddingTop="4dp"
- android:paddingBottom="4dp"
- android:gravity="center_vertical"
+ android:paddingTop="2dp"
+ android:paddingBottom="2dp"
+ android:gravity="top"
>
<LinearLayout
android:layout_width="match_parent"
@@ -50,6 +50,7 @@
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:paddingTop="6dp"
android:orientation="horizontal"
android:layout_gravity="top"
android:layout_weight="0"
@@ -92,11 +93,18 @@
android:layout_weight="0"
android:visibility="gone"
/>
+ <ProgressBar
+ android:id="@android:id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="12dp"
+ android:visibility="gone"
+ android:layout_weight="0"
+ style="?android:attr/progressBarStyleHorizontal"
+ />
<TextView android:id="@+id/big_text"
android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:singleLine="false"
android:visibility="gone"
@@ -113,7 +121,7 @@
android:layout_weight="1"
/>
<TextView android:id="@+id/overflow_title"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title"
+ android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
@@ -127,7 +135,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
- android:layout_weight="1"
+ android:layout_weight="0"
>
<TextView android:id="@+id/text"
android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
@@ -151,22 +159,14 @@
/>
<ImageView android:id="@+id/right_icon"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="0"
- android:scaleType="center"
+ android:scaleType="centerInside"
android:paddingLeft="8dp"
android:visibility="gone"
android:drawableAlpha="180"
/>
</LinearLayout>
- <ProgressBar
- android:id="@android:id/progress"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone"
- android:layout_weight="0"
- style="?android:attr/progressBarStyleHorizontal"
- />
</LinearLayout>
</FrameLayout>
diff --git a/core/res/res/layout/notification_template_inbox.xml b/core/res/res/layout/notification_template_inbox.xml
index 3b00feb..121a81c 100644
--- a/core/res/res/layout/notification_template_inbox.xml
+++ b/core/res/res/layout/notification_template_inbox.xml
@@ -38,14 +38,15 @@
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp"
- android:paddingTop="4dp"
- android:paddingBottom="4dp"
- android:gravity="center_vertical"
+ android:paddingTop="2dp"
+ android:paddingBottom="2dp"
+ android:gravity="top"
>
<LinearLayout
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:paddingTop="6dp"
android:orientation="horizontal"
android:layout_weight="0"
>
@@ -87,6 +88,14 @@
android:visibility="gone"
android:layout_weight="0"
/>
+ <ProgressBar
+ android:id="@android:id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="12dp"
+ android:visibility="gone"
+ android:layout_weight="0"
+ style="?android:attr/progressBarStyleHorizontal"
+ />
<TextView android:id="@+id/inbox_text0"
android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
@@ -168,7 +177,7 @@
android:layout_weight="0"
/>
<TextView android:id="@+id/overflow_title"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title"
+ android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
@@ -206,22 +215,14 @@
/>
<ImageView android:id="@+id/right_icon"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="0"
- android:scaleType="center"
+ android:scaleType="centerInside"
android:paddingLeft="8dp"
android:visibility="gone"
android:drawableAlpha="180"
/>
</LinearLayout>
- <ProgressBar
- android:id="@android:id/progress"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone"
- android:layout_weight="0"
- style="?android:attr/progressBarStyleHorizontal"
- />
</LinearLayout>
</FrameLayout>
diff --git a/core/res/res/raw/accessibility_gestures.bin b/core/res/res/raw/accessibility_gestures.bin
index 96fa1ec..acd7993 100644
--- a/core/res/res/raw/accessibility_gestures.bin
+++ b/core/res/res/raw/accessibility_gestures.bin
Binary files differ
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 734151b..d549644 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -227,4 +227,11 @@
action bar tabs from becoming too wide on a wide screen when only
a few are present. -->
<dimen name="action_bar_stacked_tab_max_width">180dp</dimen>
+
+ <!-- Size of notification text (see TextAppearance.StatusBar.EventContent) -->
+ <dimen name="notification_text_size">14dp</dimen>
+ <!-- Size of notification text titles (see TextAppearance.StatusBar.EventContent.Title) -->
+ <dimen name="notification_title_text_size">18dp</dimen>
+ <!-- Size of smaller notification text (see TextAppearance.StatusBar.EventContent.Line2, Info, Time) -->
+ <dimen name="notification_subtext_size">12dp</dimen>
</resources>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 9829d89..f1f67eb 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -297,6 +297,9 @@
<java-symbol type="dimen" name="volume_panel_top" />
<java-symbol type="dimen" name="action_bar_stacked_max_height" />
<java-symbol type="dimen" name="action_bar_stacked_tab_max_width" />
+ <java-symbol type="dimen" name="notification_text_size" />
+ <java-symbol type="dimen" name="notification_title_text_size" />
+ <java-symbol type="dimen" name="notification_subtext_size" />
<java-symbol type="string" name="addToDictionary" />
<java-symbol type="string" name="action_bar_home_description" />
@@ -1518,6 +1521,8 @@
<java-symbol type="xml" name="storage_list" />
<java-symbol type="bool" name="config_enableDreams" />
<java-symbol type="string" name="config_defaultDreamComponent" />
+ <java-symbol type="string" name="enable_explore_by_touch_warning_title" />
+ <java-symbol type="string" name="enable_explore_by_touch_warning_message" />
<java-symbol type="layout" name="resolver_grid" />
<java-symbol type="id" name="resolver_grid" />
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d50e2de..5929439 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2503,6 +2503,25 @@
<!-- SearchView accessibility description for voice button [CHAR LIMIT=NONE] -->
<string name="searchview_description_voice">Voice search</string>
+ <!-- Title for a warning message about the interaction model changes after allowing an accessibility
+ service to put the device into explore by touch mode, displayed as a dialog message when
+ the user selects to enables the service. (default). [CHAR LIMIT=35] -->
+ <string name="enable_explore_by_touch_warning_title">Enable Explore by Touch?</string>
+ <!-- Summary for a warning message about the interaction model changes after allowing an accessibility
+ service to put the device into explore by touch mode, displayed as a dialog message when
+ the user selects to enables the service. (tablet). [CHAR LIMIT=NONE] -->
+ <string name="enable_explore_by_touch_warning_message" product="tablet">
+ <xliff:g id="accessibility_service_name">%1$s</xliff:g> wants to enable Explore by Touch.
+ When Explore by Touch is turned on, you can hear or see descriptions of what\'s under
+ your finger or perform gestures to interact with the tablet.</string>
+ <!-- Summary for a warning message about the interaction model changes after allowing an accessibility
+ service to put the device into explore by touch mode, displayed as a dialog message when
+ the user selects to enables the service. (default). [CHAR LIMIT=NONE] -->
+ <string name="enable_explore_by_touch_warning_message" product="default">
+ <xliff:g id="accessibility_service_name">%1$s</xliff:g> wants to enable Explore by Touch.
+ When Explore by Touch is turned on, you can hear or see descriptions of what\'s under
+ your finger or perform gestures to interact with the phone.</string>
+
<!-- String used to display the date. This is the string to say something happened 1 month ago. -->
<string name="oneMonthDurationPast">1 month ago</string>
<!-- String used to display the date. This is the string to say something happened more than 1 month ago. -->
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index a90dab8..a54cdf1 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -240,23 +240,23 @@
<!-- Notification content styles -->
<style name="TextAppearance.StatusBar.EventContent">
<item name="android:textColor">#808080</item>
- <item name="android:textSize">14dp</item>
+ <item name="android:textSize">@dimen/notification_text_size</item>
</style>
<style name="TextAppearance.StatusBar.EventContent.Title">
<item name="android:textColor">#ffffff</item>
<item name="android:fontFamily">sans-serif-light</item>
- <item name="android:textSize">18dp</item>
+ <item name="android:textSize">@dimen/notification_title_text_size</item>
<item name="android:textStyle">bold</item>
</style>
<style name="TextAppearance.StatusBar.EventContent.Line2">
- <!-- inherit all -->
+ <item name="android:textSize">@dimen/notification_subtext_size</item>
</style>
<style name="TextAppearance.StatusBar.EventContent.Info">
- <item name="android:textSize">12sp</item>
+ <item name="android:textSize">@dimen/notification_subtext_size</item>
<item name="android:textColor">#666666</item>
</style>
<style name="TextAppearance.StatusBar.EventContent.Time">
- <item name="android:textSize">12sp</item>
+ <item name="android:textSize">@dimen/notification_subtext_size</item>
<item name="android:textColor">#666666</item>
</style>
diff --git a/packages/SystemUI/res/anim/search_launch_enter.xml b/packages/SystemUI/res/anim/search_launch_enter.xml
new file mode 100644
index 0000000..055ea5d
--- /dev/null
+++ b/packages/SystemUI/res/anim/search_launch_enter.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shareInterpolator="false" android:zAdjustment="top">
+
+ <alpha android:fromAlpha="0" android:toAlpha="1.0"
+ android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:duration="300"/>
+
+ <translate android:fromYDelta="200" android:toYDelta="0"
+ android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true"
+ android:interpolator="@android:interpolator/decelerate_cubic"
+ android:duration="300" />
+</set>
diff --git a/packages/SystemUI/res/anim/search_launch_exit.xml b/packages/SystemUI/res/anim/search_launch_exit.xml
new file mode 100644
index 0000000..b4ed278
--- /dev/null
+++ b/packages/SystemUI/res/anim/search_launch_exit.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:fromXDelta="0" android:toXDelta="0"
+ android:duration="300" />
diff --git a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java
index 6b0bb87..28283ef4 100644
--- a/packages/SystemUI/src/com/android/systemui/SearchPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/SearchPanelView.java
@@ -18,6 +18,8 @@
import android.animation.Animator;
import android.animation.LayoutTransition;
+import android.app.ActivityManagerNative;
+import android.app.ActivityOptions;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
@@ -36,6 +38,7 @@
import com.android.internal.widget.multiwaveview.MultiWaveView;
import com.android.internal.widget.multiwaveview.MultiWaveView.OnTriggerListener;
+import com.android.server.am.ActivityManagerService;
import com.android.systemui.R;
import com.android.systemui.recent.StatusBarTouchProxy;
import com.android.systemui.statusbar.BaseStatusBar;
@@ -103,26 +106,22 @@
}
private void startAssistActivity() {
- if (mSearchManager != null) {
- ComponentName globalSearchActivity = mSearchManager.getGlobalSearchActivity();
- if (globalSearchActivity != null) {
- Intent intent = new Intent(Intent.ACTION_ASSIST);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setPackage(globalSearchActivity.getPackageName());
- try {
- mContext.startActivity(intent);
- } catch (ActivityNotFoundException e) {
- Slog.w(TAG, "Activity not found for " + intent.getAction());
- }
- } else {
- Slog.w(TAG, "No global search activity");
- }
+ Intent intent = getAssistIntent();
+ try {
+ ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
+ R.anim.search_launch_enter, R.anim.search_launch_exit);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent, opts.toBundle());
+ } catch (ActivityNotFoundException e) {
+ Slog.w(TAG, "Activity not found for " + intent.getAction());
}
}
final MultiWaveView.OnTriggerListener mMultiWaveViewListener
= new MultiWaveView.OnTriggerListener() {
+ private int mTarget = -1;
+
public void onGrabbed(View v, int handle) {
}
@@ -136,11 +135,18 @@
}
public void onTrigger(View v, int target) {
- final int resId = mMultiWaveView.getResourceIdForTarget(target);
- switch (resId) {
- case com.android.internal.R.drawable.ic_lockscreen_search:
- startAssistActivity();
- break;
+ mTarget = target;
+ }
+
+ public void onFinishFinalAnimation() {
+ if (mTarget != -1) {
+ final int resId = mMultiWaveView.getResourceIdForTarget(mTarget);
+ mTarget = -1; // a safety to make sure we never launch w/o prior call to onTrigger
+ switch (resId) {
+ case com.android.internal.R.drawable.ic_lockscreen_search:
+ startAssistActivity();
+ break;
+ }
}
mBar.hideSearchPanel();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 69d2e73..d38611d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -515,14 +515,8 @@
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
- Slog.d(TAG, "showing search panel");
showSearchPanel();
break;
-
- case MotionEvent.ACTION_UP:
- Slog.d(TAG, "hiding search panel");
- hideSearchPanel();
- break;
}
return false;
}
@@ -533,8 +527,8 @@
mNavigationBarView.getRecentsButton().setOnClickListener(mRecentsClickListener);
mNavigationBarView.getRecentsButton().setOnTouchListener(mRecentsPanel);
+ mNavigationBarView.getHomeButton().setOnTouchListener(mHomeSearchActionListener);
updateSearchPanel();
-// mNavigationBarView.getHomeButton().setOnTouchListener(mHomeSearchActionListener);
}
// For small-screen devices (read: phones) that lack hardware navigation buttons
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java
index dba1606..10c5dd8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java
@@ -188,6 +188,17 @@
public Context getContext() { return mContext; }
+ private View.OnTouchListener mHomeSearchActionListener = new View.OnTouchListener() {
+ public boolean onTouch(View v, MotionEvent event) {
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ showSearchPanel();
+ break;
+ }
+ return false;
+ }
+ };
+
@Override
protected void createAndAddWindows() {
addStatusBarWindow();
@@ -290,6 +301,7 @@
// Search Panel
mStatusBarView.setBar(this);
+ mHomeButton.setOnTouchListener(mHomeSearchActionListener);
updateSearchPanel();
// Input methods Panel
diff --git a/policy/src/com/android/internal/policy/impl/LockScreen.java b/policy/src/com/android/internal/policy/impl/LockScreen.java
index 22f70a5..30cb530 100644
--- a/policy/src/com/android/internal/policy/impl/LockScreen.java
+++ b/policy/src/com/android/internal/policy/impl/LockScreen.java
@@ -401,6 +401,10 @@
public void cleanUp() {
mMultiWaveView.setOnTriggerListener(null);
}
+
+ public void onFinishFinalAnimation() {
+
+ }
}
private void requestUnlockScreen() {
diff --git a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 3fbac38..3e35b20d 100644
--- a/services/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -16,7 +16,6 @@
package com.android.server.accessibility;
-import com.android.server.accessibility.TouchExplorer.GestureListener;
import com.android.server.input.InputFilter;
import android.content.Context;
@@ -40,7 +39,7 @@
private final PowerManager mPm;
- private final GestureListener mGestureListener;
+ private final AccessibilityManagerService mAms;
/**
* This is an interface for explorers that take a {@link MotionEvent}
@@ -73,10 +72,10 @@
private int mTouchscreenSourceDeviceId;
- public AccessibilityInputFilter(Context context, GestureListener gestureListener) {
+ public AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
super(context.getMainLooper());
mContext = context;
- mGestureListener = gestureListener;
+ mAms = service;
mPm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
}
@@ -85,7 +84,7 @@
if (DEBUG) {
Slog.d(TAG, "Accessibility input filter installed.");
}
- mTouchExplorer = new TouchExplorer(this, mContext, mGestureListener);
+ mTouchExplorer = new TouchExplorer(this, mContext, mAms);
super.onInstalled();
}
diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
index f23b25e..ebc2074 100644
--- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -24,12 +24,15 @@
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
@@ -58,7 +61,9 @@
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
@@ -66,8 +71,8 @@
import android.view.accessibility.IAccessibilityManager;
import android.view.accessibility.IAccessibilityManagerClient;
+import com.android.internal.R;
import com.android.internal.content.PackageMonitor;
-import com.android.server.accessibility.TouchExplorer.GestureListener;
import com.android.server.wm.WindowManagerService;
import org.xmlpull.v1.XmlPullParserException;
@@ -90,8 +95,7 @@
*
* @hide
*/
-public class AccessibilityManagerService extends IAccessibilityManager.Stub
- implements GestureListener {
+public class AccessibilityManagerService extends IAccessibilityManager.Stub {
private static final boolean DEBUG = false;
@@ -102,6 +106,8 @@
private static final int OWN_PROCESS_ID = android.os.Process.myPid();
+ private static final int MSG_SHOW_ENABLE_TOUCH_EXPLORATION_DIALOG = 1;
+
private static int sIdCounter = 0;
private static int sNextWindowId;
@@ -128,6 +134,8 @@
private final SimpleStringSplitter mStringColonSplitter = new SimpleStringSplitter(':');
+ private final Rect mTempRect = new Rect();
+
private PackageManager mPackageManager;
private int mHandledFeedbackTypes = 0;
@@ -146,23 +154,15 @@
private final SecurityPolicy mSecurityPolicy;
+ private final MainHanler mMainHandler;
+
private Service mUiAutomationService;
- /**
- * Handler for delayed event dispatch.
- */
- private Handler mHandler = new Handler() {
+ private Service mQueryBridge;
- @Override
- public void handleMessage(Message message) {
- Service service = (Service) message.obj;
- int eventType = message.arg1;
+ private boolean mTouchExplorationGestureEnded;
- synchronized (mLock) {
- notifyAccessibilityEventLocked(service, eventType);
- }
- }
- };
+ private boolean mTouchExplorationGestureStarted;
/**
* Creates a new instance.
@@ -175,7 +175,7 @@
mWindowManagerService = (WindowManagerService) ServiceManager.getService(
Context.WINDOW_SERVICE);
mSecurityPolicy = new SecurityPolicy();
-
+ mMainHandler = new MainHanler();
registerPackageChangeAndBootCompletedBroadcastReceiver();
registerSettingsContentObservers();
}
@@ -349,15 +349,37 @@
}
public boolean sendAccessibilityEvent(AccessibilityEvent event) {
+ // The event for gesture start should be strictly before the
+ // first hover enter event for the gesture.
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
+ && mTouchExplorationGestureStarted) {
+ mTouchExplorationGestureStarted = false;
+ AccessibilityEvent gestureStartEvent = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START);
+ sendAccessibilityEvent(gestureStartEvent);
+ }
+
synchronized (mLock) {
if (mSecurityPolicy.canDispatchAccessibilityEvent(event)) {
- mSecurityPolicy.updateRetrievalAllowingWindowAndEventSourceLocked(event);
+ mSecurityPolicy.updateActiveWindowAndEventSourceLocked(event);
notifyAccessibilityServicesDelayedLocked(event, false);
notifyAccessibilityServicesDelayedLocked(event, true);
}
}
+
+ // The event for gesture end should be strictly after the
+ // last hover exit event for the gesture.
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
+ && mTouchExplorationGestureEnded) {
+ mTouchExplorationGestureEnded = false;
+ AccessibilityEvent gestureEndEvent = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END);
+ sendAccessibilityEvent(gestureEndEvent);
+ }
+
event.recycle();
mHandledFeedbackTypes = 0;
+
return (OWN_PROCESS_ID != Binder.getCallingPid());
}
@@ -472,8 +494,7 @@
}
}
- @Override
- public boolean onGesture(int gestureId) {
+ boolean onGesture(int gestureId) {
synchronized (mLock) {
boolean handled = notifyGestureLocked(gestureId, false);
if (!handled) {
@@ -483,6 +504,65 @@
}
}
+ /**
+ * Gets the bounds of the accessibility focus if the provided,
+ * point coordinates are within the currently active window
+ * and accessibility focus is found within the latter.
+ *
+ * @param x X coordinate.
+ * @param y Y coordinate
+ * @param outBounds The output to which to write the focus bounds.
+ * @return The accessibility focus rectangle if the point is in the
+ * window and the window has accessibility focus.
+ */
+ boolean getAccessibilityFocusBounds(int x, int y, Rect outBounds) {
+ // Instead of keeping track of accessibility focus events per
+ // window to be able to find the focus in the active window,
+ // we take a stateless approach and look it up. This is fine
+ // since we do this only when the user clicks/long presses.
+ Service service = getQueryBridge();
+ final int connectionId = service.mId;
+ AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
+ client.addConnection(connectionId, service);
+ try {
+ AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance()
+ .getRootInActiveWindow(connectionId);
+ if (root == null) {
+ return false;
+ }
+ Rect bounds = mTempRect;
+ root.getBoundsInScreen(bounds);
+ if (!bounds.contains(x, y)) {
+ return false;
+ }
+ AccessibilityNodeInfo focus = root.findFocus(
+ AccessibilityNodeInfo.FOCUS_ACCESSIBILITY);
+ if (focus == null) {
+ return false;
+ }
+ focus.getBoundsInScreen(outBounds);
+ return true;
+ } finally {
+ client.removeConnection(connectionId);
+ }
+ }
+
+ private Service getQueryBridge() {
+ if (mQueryBridge == null) {
+ AccessibilityServiceInfo info = new AccessibilityServiceInfo();
+ mQueryBridge = new Service(null, info, true);
+ }
+ return mQueryBridge;
+ }
+
+ public void touchExplorationGestureEnded() {
+ mTouchExplorationGestureEnded = true;
+ }
+
+ public void touchExplorationGestureStarted() {
+ mTouchExplorationGestureStarted = true;
+ }
+
private boolean notifyGestureLocked(int gestureId, boolean isDefault) {
// TODO: Now we are giving the gestures to the last enabled
// service that can handle them which is the last one
@@ -496,12 +576,7 @@
for (int i = mServices.size() - 1; i >= 0; i--) {
Service service = mServices.get(i);
if (service.mReqeustTouchExplorationMode && service.mIsDefault == isDefault) {
- try {
- service.mServiceInterface.onGesture(gestureId);
- } catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error during sending gesture " + gestureId
- + " to " + service.mService, re);
- }
+ service.notifyGesture(gestureId);
return true;
}
}
@@ -573,7 +648,7 @@
if (service.mIsDefault == isDefault) {
if (canDispathEventLocked(service, event, mHandledFeedbackTypes)) {
mHandledFeedbackTypes |= service.mFeedbackType;
- notifyAccessibilityServiceDelayedLocked(service, event);
+ service.notifyAccessibilityEvent(event);
}
}
}
@@ -586,90 +661,6 @@
}
/**
- * Performs an {@link AccessibilityService} delayed notification. The delay is configurable
- * and denotes the period after the last event before notifying the service.
- *
- * @param service The service.
- * @param event The event.
- */
- private void notifyAccessibilityServiceDelayedLocked(Service service,
- AccessibilityEvent event) {
- synchronized (mLock) {
- final int eventType = event.getEventType();
- // Make a copy since during dispatch it is possible the event to
- // be modified to remove its source if the receiving service does
- // not have permission to access the window content.
- AccessibilityEvent newEvent = AccessibilityEvent.obtain(event);
- AccessibilityEvent oldEvent = service.mPendingEvents.get(eventType);
- service.mPendingEvents.put(eventType, newEvent);
-
- final int what = eventType | (service.mId << 16);
- if (oldEvent != null) {
- mHandler.removeMessages(what);
- oldEvent.recycle();
- }
-
- Message message = mHandler.obtainMessage(what, service);
- message.arg1 = eventType;
- mHandler.sendMessageDelayed(message, service.mNotificationTimeout);
- }
- }
-
- /**
- * Notifies an accessibility service client for a scheduled event given the event type.
- *
- * @param service The service client.
- * @param eventType The type of the event to dispatch.
- */
- private void notifyAccessibilityEventLocked(Service service, int eventType) {
- IAccessibilityServiceClient listener = service.mServiceInterface;
-
- // If the service died/was disabled while the message for dispatching
- // the accessibility event was propagating the listener may be null.
- if (listener == null) {
- return;
- }
-
- AccessibilityEvent event = service.mPendingEvents.get(eventType);
-
- // Check for null here because there is a concurrent scenario in which this
- // happens: 1) A binder thread calls notifyAccessibilityServiceDelayedLocked
- // which posts a message for dispatching an event. 2) The message is pulled
- // from the queue by the handler on the service thread and the latter is
- // just about to acquire the lock and call this method. 3) Now another binder
- // thread acquires the lock calling notifyAccessibilityServiceDelayedLocked
- // so the service thread waits for the lock; 4) The binder thread replaces
- // the event with a more recent one (assume the same event type) and posts a
- // dispatch request releasing the lock. 5) Now the main thread is unblocked and
- // dispatches the event which is removed from the pending ones. 6) And ... now
- // the service thread handles the last message posted by the last binder call
- // but the event is already dispatched and hence looking it up in the pending
- // ones yields null. This check is much simpler that keeping count for each
- // event type of each service to catch such a scenario since only one message
- // is processed at a time.
- if (event == null) {
- return;
- }
-
- service.mPendingEvents.remove(eventType);
- try {
- if (mSecurityPolicy.canRetrieveWindowContent(service)) {
- event.setConnectionId(service.mId);
- } else {
- event.setSource(null);
- }
- event.setSealed(true);
- listener.onAccessibilityEvent(event);
- event.recycle();
- if (DEBUG) {
- Slog.i(LOG_TAG, "Event " + event + " sent to " + listener);
- }
- } catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error during sending " + event + " to " + service.mService, re);
- }
- }
-
- /**
* Adds a service.
*
* @param service The service to add.
@@ -683,6 +674,7 @@
mServices.add(service);
mComponentNameToServiceMap.put(service.mComponentName, service);
updateInputFilterLocked();
+ tryEnableTouchExploration(service);
} catch (RemoteException e) {
/* do nothing */
}
@@ -700,10 +692,10 @@
return false;
}
mComponentNameToServiceMap.remove(service.mComponentName);
- mHandler.removeMessages(service.mId);
service.unlinkToOwnDeath();
service.dispose();
updateInputFilterLocked();
+ tryDisableTouchExploration(service);
return removed;
}
@@ -932,6 +924,29 @@
Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0) == 1;
}
+ private void tryEnableTouchExploration(final Service service) {
+ if (!mIsTouchExplorationEnabled && service.mRequestTouchExplorationMode) {
+ mMainHandler.obtainMessage(MSG_SHOW_ENABLE_TOUCH_EXPLORATION_DIALOG,
+ service).sendToTarget();
+ }
+ }
+
+ private void tryDisableTouchExploration(Service service) {
+ if (mIsTouchExplorationEnabled && service.mReqeustTouchExplorationMode) {
+ synchronized (mLock) {
+ final int serviceCount = mServices.size();
+ for (int i = 0; i < serviceCount; i++) {
+ Service other = mServices.get(i);
+ if (other != service && other.mRequestTouchExplorationMode) {
+ return;
+ }
+ }
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0);
+ }
+ }
+ }
+
private class AccessibilityConnectionWrapper implements DeathRecipient {
private final int mWindowId;
private final IAccessibilityInteractionConnection mConnection;
@@ -959,6 +974,42 @@
}
}
+ private class MainHanler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ final int type = msg.what;
+ switch (type) {
+ case MSG_SHOW_ENABLE_TOUCH_EXPLORATION_DIALOG: {
+ Service service = (Service) msg.obj;
+ String label = service.mResolveInfo.loadLabel(
+ mContext.getPackageManager()).toString();
+ final AlertDialog dialog = new AlertDialog.Builder(mContext)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.TOUCH_EXPLORATION_ENABLED, 1);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .setTitle(R.string.enable_explore_by_touch_warning_title)
+ .setMessage(mContext.getString(
+ R.string.enable_explore_by_touch_warning_message, label))
+ .create();
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
+ dialog.setCanceledOnTouchOutside(true);
+ dialog.show();
+ }
+ }
+ }
+ }
+
/**
* This class represents an accessibility service. It stores all per service
* data required for the service management, provides API for starting/stopping the
@@ -969,6 +1020,11 @@
*/
class Service extends IAccessibilityServiceConnection.Stub
implements ServiceConnection, DeathRecipient {
+
+ // We pick the MSB to avoid collision since accessibility event types are
+ // used as message types allowing us to remove messages per event type.
+ private static final int MSG_ON_GESTURE = 0x80000000;
+
int mId = 0;
AccessibilityServiceInfo mAccessibilityServiceInfo;
@@ -985,6 +1041,8 @@
boolean mIsDefault;
+ boolean mRequestTouchExplorationMode;
+
boolean mIncludeNotImportantViews;
long mNotificationTimeout;
@@ -1001,12 +1059,35 @@
final Rect mTempBounds = new Rect();
+ final ResolveInfo mResolveInfo;
+
// the events pending events to be dispatched to this service
final SparseArray<AccessibilityEvent> mPendingEvents =
new SparseArray<AccessibilityEvent>();
+ /**
+ * Handler for delayed event dispatch.
+ */
+ public Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ final int type = message.what;
+ switch (type) {
+ case MSG_ON_GESTURE: {
+ final int gestureId = message.arg1;
+ notifyGestureInternal(gestureId);
+ } break;
+ default: {
+ final int eventType = type;
+ notifyAccessibilityEventInternal(eventType);
+ } break;
+ }
+ }
+ };
+
public Service(ComponentName componentName,
AccessibilityServiceInfo accessibilityServiceInfo, boolean isAutomation) {
+ mResolveInfo = accessibilityServiceInfo.getResolveInfo();
mId = sIdCounter++;
mComponentName = componentName;
mAccessibilityServiceInfo = accessibilityServiceInfo;
@@ -1043,6 +1124,9 @@
(info.flags & FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0;
}
+ mRequestTouchExplorationMode = (info.flags
+ & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0;
+
synchronized (mLock) {
tryAddServiceLocked(this);
}
@@ -1403,6 +1487,108 @@
}
}
+ /**
+ * Performs a notification for an {@link AccessibilityEvent}.
+ *
+ * @param event The event.
+ */
+ public void notifyAccessibilityEvent(AccessibilityEvent event) {
+ synchronized (mLock) {
+ final int eventType = event.getEventType();
+ // Make a copy since during dispatch it is possible the event to
+ // be modified to remove its source if the receiving service does
+ // not have permission to access the window content.
+ AccessibilityEvent newEvent = AccessibilityEvent.obtain(event);
+ AccessibilityEvent oldEvent = mPendingEvents.get(eventType);
+ mPendingEvents.put(eventType, newEvent);
+
+ final int what = eventType;
+ if (oldEvent != null) {
+ mHandler.removeMessages(what);
+ oldEvent.recycle();
+ }
+
+ Message message = mHandler.obtainMessage(what);
+ mHandler.sendMessageDelayed(message, mNotificationTimeout);
+ }
+ }
+
+ /**
+ * Notifies an accessibility service client for a scheduled event given the event type.
+ *
+ * @param eventType The type of the event to dispatch.
+ */
+ private void notifyAccessibilityEventInternal(int eventType) {
+ IAccessibilityServiceClient listener;
+ AccessibilityEvent event;
+
+ synchronized (mLock) {
+ listener = mServiceInterface;
+
+ // If the service died/was disabled while the message for dispatching
+ // the accessibility event was propagating the listener may be null.
+ if (listener == null) {
+ return;
+ }
+
+ event = mPendingEvents.get(eventType);
+
+ // Check for null here because there is a concurrent scenario in which this
+ // happens: 1) A binder thread calls notifyAccessibilityServiceDelayedLocked
+ // which posts a message for dispatching an event. 2) The message is pulled
+ // from the queue by the handler on the service thread and the latter is
+ // just about to acquire the lock and call this method. 3) Now another binder
+ // thread acquires the lock calling notifyAccessibilityServiceDelayedLocked
+ // so the service thread waits for the lock; 4) The binder thread replaces
+ // the event with a more recent one (assume the same event type) and posts a
+ // dispatch request releasing the lock. 5) Now the main thread is unblocked and
+ // dispatches the event which is removed from the pending ones. 6) And ... now
+ // the service thread handles the last message posted by the last binder call
+ // but the event is already dispatched and hence looking it up in the pending
+ // ones yields null. This check is much simpler that keeping count for each
+ // event type of each service to catch such a scenario since only one message
+ // is processed at a time.
+ if (event == null) {
+ return;
+ }
+
+ mPendingEvents.remove(eventType);
+ if (mSecurityPolicy.canRetrieveWindowContent(this)) {
+ event.setConnectionId(mId);
+ } else {
+ event.setSource(null);
+ }
+ event.setSealed(true);
+ }
+
+ try {
+ listener.onAccessibilityEvent(event);
+ if (DEBUG) {
+ Slog.i(LOG_TAG, "Event " + event + " sent to " + listener);
+ }
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re);
+ } finally {
+ event.recycle();
+ }
+ }
+
+ public void notifyGesture(int gestureId) {
+ mHandler.obtainMessage(MSG_ON_GESTURE, gestureId, 0).sendToTarget();
+ }
+
+ private void notifyGestureInternal(int gestureId) {
+ IAccessibilityServiceClient listener = mServiceInterface;
+ if (listener != null) {
+ try {
+ listener.onGesture(gestureId);
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Error during sending gesture " + gestureId
+ + " to " + mService, re);
+ }
+ }
+ }
+
private void sendDownAndUpKeyEvents(int keyCode) {
final long token = Binder.clearCallingIdentity();
@@ -1454,7 +1640,7 @@
private int resolveAccessibilityWindowId(int accessibilityWindowId) {
if (accessibilityWindowId == AccessibilityNodeInfo.ACTIVE_WINDOW_ID) {
- return mSecurityPolicy.mRetrievalAlowingWindowId;
+ return mSecurityPolicy.mActiveWindowId;
}
return accessibilityWindowId;
}
@@ -1497,24 +1683,35 @@
| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
- private static final int RETRIEVAL_ALLOWING_WINDOW_CHANGE_EVENT_TYPES =
- AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
- | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
- | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT;
-
- private int mRetrievalAlowingWindowId;
+ private int mActiveWindowId;
private boolean canDispatchAccessibilityEvent(AccessibilityEvent event) {
// Send window changed event only for the retrieval allowing window.
return (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
- || event.getWindowId() == mRetrievalAlowingWindowId);
+ || event.getWindowId() == mActiveWindowId);
}
- public void updateRetrievalAllowingWindowAndEventSourceLocked(AccessibilityEvent event) {
+ public void updateActiveWindowAndEventSourceLocked(AccessibilityEvent event) {
+ // The active window is either the window that has input focus or
+ // the window that the user is currently touching. If the user is
+ // touching a window that does not have input focus as soon as the
+ // the user stops touching that window the focused window becomes
+ // the active one.
final int windowId = event.getWindowId();
final int eventType = event.getEventType();
- if ((eventType & RETRIEVAL_ALLOWING_WINDOW_CHANGE_EVENT_TYPES) != 0) {
- mRetrievalAlowingWindowId = windowId;
+ switch (eventType) {
+ case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
+ if (getFocusedWindowId() == windowId) {
+ mActiveWindowId = windowId;
+ }
+ } break;
+ case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
+ case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: {
+ mActiveWindowId = windowId;
+ } break;
+ case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: {
+ mActiveWindowId = getFocusedWindowId();
+ } break;
}
if ((eventType & RETRIEVAL_ALLOWING_EVENT_TYPES) == 0) {
event.setSource(null);
@@ -1522,7 +1719,7 @@
}
public int getRetrievalAllowingWindowLocked() {
- return mRetrievalAlowingWindowId;
+ return mActiveWindowId;
}
public boolean canGetAccessibilityNodeInfoLocked(Service service, int windowId) {
@@ -1550,7 +1747,7 @@
}
private boolean isRetrievalAllowingWindow(int windowId) {
- return (mRetrievalAlowingWindowId == windowId);
+ return (mActiveWindowId == windowId);
}
private boolean isActionPermitted(int action) {
@@ -1567,5 +1764,22 @@
+ " required to call " + function);
}
}
+
+ private int getFocusedWindowId() {
+ // We call this only on window focus change or after touch
+ // exploration gesture end and the shown windows are not that
+ // many, so the linear look up is just fine.
+ IBinder token = mWindowManagerService.getFocusedWindowClientToken();
+ if (token != null) {
+ SparseArray<IBinder> windows = mWindowIdToWindowTokenMap;
+ final int windowCount = windows.size();
+ for (int i = 0; i < windowCount; i++) {
+ if (windows.valueAt(i) == token) {
+ return windows.keyAt(i);
+ }
+ }
+ }
+ return -1;
+ }
}
}
diff --git a/services/java/com/android/server/accessibility/TouchExplorer.java b/services/java/com/android/server/accessibility/TouchExplorer.java
index 39012e6..b0b2b8d 100644
--- a/services/java/com/android/server/accessibility/TouchExplorer.java
+++ b/services/java/com/android/server/accessibility/TouchExplorer.java
@@ -16,9 +16,6 @@
package com.android.server.accessibility;
-import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END;
-import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START;
-
import android.content.Context;
import android.gesture.Gesture;
import android.gesture.GestureLibraries;
@@ -26,17 +23,19 @@
import android.gesture.GesturePoint;
import android.gesture.GestureStroke;
import android.gesture.Prediction;
+import android.graphics.Rect;
import android.os.Handler;
+import android.os.SystemClock;
import android.util.Slog;
import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.WindowManagerPolicy;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
-import com.android.server.input.InputFilter;
import com.android.internal.R;
+import com.android.server.input.InputFilter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -47,17 +46,18 @@
* and consuming certain events. The interaction model is:
*
* <ol>
- * <li>1. One finger moving around performs touch exploration.</li>
- * <li>2. Two close fingers moving in the same direction perform a drag.</li>
- * <li>3. Multi-finger gestures are delivered to view hierarchy.</li>
- * <li>4. Pointers that have not moved more than a specified distance after they
+ * <li>1. One finger moving slow around performs touch exploration.</li>
+ * <li>2. One finger moving fast around performs gestures.</li>
+ * <li>3. Two close fingers moving in the same direction perform a drag.</li>
+ * <li>4. Multi-finger gestures are delivered to view hierarchy.</li>
+ * <li>5. Pointers that have not moved more than a specified distance after they
* went down are considered inactive.</li>
- * <li>5. Two fingers moving too far from each other or in different directions
- * are considered a multi-finger gesture.</li>
- * <li>6. Tapping on the last touch explored location within given time and
- * distance slop performs a click.</li>
- * <li>7. Tapping and holding for a while on the last touch explored location within
- * given time and distance slop performs a long press.</li>
+ * <li>6. Two fingers moving in different directions are considered a multi-finger gesture.</li>
+ * <li>7. Double tapping clicks on the on the last touch explored location of it was in
+ * a window that does not take focus, otherwise the click is within the accessibility
+ * focused rectangle.</li>
+ * <li>7. Tapping and holding for a while performs a long press in a similar fashion
+ * as the click above.</li>
* <ol>
*
* @hide
@@ -75,85 +75,116 @@
private static final int STATE_DELEGATING = 0x00000004;
private static final int STATE_GESTURE_DETECTING = 0x00000005;
- // The time slop in milliseconds for activating an item after it has
- // been touch explored. Tapping on an item within this slop will perform
- // a click and tapping and holding down a long press.
- private static final long ACTIVATION_TIME_SLOP = 2000;
-
// The minimum of the cosine between the vectors of two moving
// pointers so they can be considered moving in the same direction.
private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4)
- // The delay for sending a hover enter event.
- private static final long DELAY_SEND_HOVER_ENTER = 200;
-
// Constant referring to the ids bits of all pointers.
private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF;
// This constant captures the current implementation detail that
// pointer IDs are between 0 and 31 inclusive (subject to change).
// (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h)
- public static final int MAX_POINTER_COUNT = 32;
+ private static final int MAX_POINTER_COUNT = 32;
// Invalid pointer ID.
- public static final int INVALID_POINTER_ID = -1;
+ private static final int INVALID_POINTER_ID = -1;
+
+ // The velocity above which we detect gestures.
+ private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000;
+
+ // The minimal distance before we take the middle of the distance between
+ // the two dragging pointers as opposed to use the location of the primary one.
+ private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200;
// Temporary array for storing pointer IDs.
private final int[] mTempPointerIds = new int[MAX_POINTER_COUNT];
- // The distance from the last touch explored location tapping within
- // which would perform a click and tapping and holding a long press.
- private final int mTouchExplorationTapSlop;
+ // Timeout within which we try to detect a tap.
+ private final int mTapTimeout;
+
+ // Timeout within which we try to detect a double tap.
+ private final int mDoubleTapTimeout;
+
+ // Slop between the down and up tap to be a tap.
+ private final int mTouchSlop;
+
+ // Slop between the first and second tap to be a double tap.
+ private final int mDoubleTapSlop;
// The InputFilter this tracker is associated with i.e. the filter
// which delegates event processing to this touch explorer.
private final InputFilter mInputFilter;
- // Handle to the accessibility manager for firing accessibility events
- // announcing touch exploration gesture start and end.
- private final AccessibilityManager mAccessibilityManager;
-
- // The last event that was received while performing touch exploration.
- private MotionEvent mLastTouchExploreEvent;
-
// The current state of the touch explorer.
private int mCurrentState = STATE_TOUCH_EXPLORING;
- // Flag whether a touch exploration gesture is in progress.
- private boolean mTouchExploreGestureInProgress;
-
// The ID of the pointer used for dragging.
private int mDraggingPointerId;
// Handler for performing asynchronous operations.
private final Handler mHandler;
- // Command for delayed sending of a hover event.
- private final SendHoverDelayed mSendHoverDelayed;
+ // Command for delayed sending of a hover enter event.
+ private final SendHoverDelayed mSendHoverEnterDelayed;
+
+ // Command for delayed sending of a hover exit event.
+ private final SendHoverDelayed mSendHoverExitDelayed;
// Command for delayed sending of a long press.
private final PerformLongPressDelayed mPerformLongPressDelayed;
+ // Helper to detect and react to double tap in touch explore mode.
+ private final DoubleTapDetector mDoubleTapDetector;
+
+ // The scaled minimal distance before we take the middle of the distance between
+ // the two dragging pointers as opposed to use the location of the primary one.
+ private final int mScaledMinPointerDistanceToUseMiddleLocation;
+
+ // The scaled velocity above which we detect gestures.
+ private final int mScaledGestureDetectionVelocity;
+
+ // Helper to track gesture velocity.
private VelocityTracker mVelocityTracker;
+ // Helper class to track received pointers.
private final ReceivedPointerTracker mReceivedPointerTracker;
+ // Helper class to track injected pointers.
private final InjectedPointerTracker mInjectedPointerTracker;
- private final GestureListener mGestureListener;
+ // Handle to the accessibility manager service.
+ private final AccessibilityManagerService mAms;
- /**
- * Callback for gesture detection.
- */
- public interface GestureListener {
+ // Temporary rectangle to avoid instantiation.
+ private final Rect mTempRect = new Rect();
- /**
- * Called when a given gesture was performed.
- *
- * @param gestureId The gesture id.
- */
- public boolean onGesture(int gestureId);
- }
+ // The X of the previous event.
+ private float mPreviousX;
+
+ // The Y of the previous event.
+ private float mPreviousY;
+
+ // Buffer for storing points for gesture detection.
+ private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);
+
+ // The minimal delta between moves to add a gesture point.
+ private static final int TOUCH_TOLERANCE = 3;
+
+ // The minimal score for accepting a predicted gesture.
+ private static final float MIN_PREDICTION_SCORE = 2.0f;
+
+ // The library for gesture detection.
+ private GestureLibrary mGestureLibrary;
+
+ // The long pressing pointer id if coordinate remapping is needed.
+ private int mLongPressingPointerId;
+
+ // The long pressing pointer X if coordinate remapping is needed.
+ private int mLongPressingPointerDeltaX;
+
+ // The long pressing pointer Y if coordinate remapping is needed.
+ private int mLongPressingPointerDeltaY;
/**
* Creates a new instance.
@@ -162,25 +193,73 @@
* @param context A context handle for accessing resources.
*/
public TouchExplorer(InputFilter inputFilter, Context context,
- GestureListener gestureListener) {
- mGestureListener = gestureListener;
+ AccessibilityManagerService service) {
+ mAms = service;
mReceivedPointerTracker = new ReceivedPointerTracker(context);
mInjectedPointerTracker = new InjectedPointerTracker();
mInputFilter = inputFilter;
- mTouchExplorationTapSlop =
- ViewConfiguration.get(context).getScaledTouchExploreTapSlop();
+ mTapTimeout = ViewConfiguration.getTapTimeout();
+ mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
mHandler = new Handler(context.getMainLooper());
- mSendHoverDelayed = new SendHoverDelayed();
mPerformLongPressDelayed = new PerformLongPressDelayed();
- mAccessibilityManager = AccessibilityManager.getInstance(context);
mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures);
mGestureLibrary.setOrientationStyle(4);
mGestureLibrary.load();
+ mSendHoverEnterDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_ENTER, true);
+ mSendHoverExitDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_EXIT, false);
+ mDoubleTapDetector = new DoubleTapDetector();
+ final float density = context.getResources().getDisplayMetrics().density;
+ mScaledMinPointerDistanceToUseMiddleLocation =
+ (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density);
+ mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density);
+ }
+
+ public void clear() {
+ // If we have not received an event then we are in initial
+ // state. Therefore, there is not need to clean anything.
+ MotionEvent event = mReceivedPointerTracker.getLastReceivedEvent();
+ if (event != null) {
+ clear(mReceivedPointerTracker.getLastReceivedEvent(), WindowManagerPolicy.FLAG_TRUSTED);
+ }
}
public void clear(MotionEvent event, int policyFlags) {
- sendUpForInjectedDownPointers(event, policyFlags);
- clear();
+ switch (mCurrentState) {
+ case STATE_TOUCH_EXPLORING: {
+ // If a touch exploration gesture is in progress send events for its end.
+ sendExitEventsIfNeeded(policyFlags);
+ } break;
+ case STATE_DRAGGING: {
+ mDraggingPointerId = INVALID_POINTER_ID;
+ // Send exit to all pointers that we have delivered.
+ sendUpForInjectedDownPointers(event, policyFlags);
+ } break;
+ case STATE_DELEGATING: {
+ // Send exit to all pointers that we have delivered.
+ sendUpForInjectedDownPointers(event, policyFlags);
+ } break;
+ case STATE_GESTURE_DETECTING: {
+ // Clear the current stroke.
+ mStrokeBuffer.clear();
+ } break;
+ }
+ // Remove all pending callbacks.
+ mSendHoverEnterDelayed.remove();
+ mSendHoverExitDelayed.remove();
+ mPerformLongPressDelayed.remove();
+ // Reset the pointer trackers.
+ mReceivedPointerTracker.clear();
+ mInjectedPointerTracker.clear();
+ // Clear the double tap detector
+ mDoubleTapDetector.clear();
+ // Go to initial state.
+ // Clear the long pressing pointer remap data.
+ mLongPressingPointerId = -1;
+ mLongPressingPointerDeltaX = 0;
+ mLongPressingPointerDeltaY = 0;
+ mCurrentState = STATE_TOUCH_EXPLORING;
}
public void onMotionEvent(MotionEvent event, int policyFlags) {
@@ -218,7 +297,6 @@
*/
private void handleMotionEventStateTouchExploring(MotionEvent event, int policyFlags) {
ReceivedPointerTracker receivedTracker = mReceivedPointerTracker;
- InjectedPointerTracker injectedTracker = mInjectedPointerTracker;
final int activePointerCount = receivedTracker.getActivePointerCount();
if (mVelocityTracker == null) {
@@ -226,8 +304,16 @@
}
mVelocityTracker.addMovement(event);
+ mDoubleTapDetector.onMotionEvent(event, policyFlags);
+
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
+ // Pre-feed the motion events to the gesture detector since we
+ // have a distance slop before getting into gesture detection
+ // mode and not using the points within this slop significantly
+ // decreases the quality of gesture recognition.
+ handleMotionEventGestureDetecting(event, policyFlags);
+ //$FALL-THROUGH$
case MotionEvent.ACTION_POINTER_DOWN: {
switch (activePointerCount) {
case 0: {
@@ -235,44 +321,31 @@
+ "touch exploring state!");
}
case 1: {
- mSendHoverDelayed.remove();
- mPerformLongPressDelayed.remove();
- // Send a hover for every finger down so the user gets feedback.
+ // If we still have not notified the user for the last
+ // touch, we figure out what to do. If were waiting
+ // we resent the delayed callback and wait again.
+ if (mSendHoverEnterDelayed.isPending()) {
+ mSendHoverEnterDelayed.remove();
+ mSendHoverExitDelayed.remove();
+ mPerformLongPressDelayed.remove();
+ }
+
+ // If we have the first tap schedule a long press and break
+ // since we do not want to schedule hover enter because
+ // the delayed callback will kick in before the long click.
+ // This would lead to a state transition resulting in long
+ // pressing the item below the double taped area which is
+ // not necessary where accessibility focus is.
+ if (mDoubleTapDetector.firstTapDetected()) {
+ // We got a tap now post a long press action.
+ mPerformLongPressDelayed.post(event, policyFlags);
+ break;
+ }
+ // Deliver hover enter with a delay to have a chance
+ // to detect what the user is trying to do.
final int pointerId = receivedTracker.getPrimaryActivePointerId();
final int pointerIdBits = (1 << pointerId);
- final int lastAction = injectedTracker.getLastInjectedHoverAction();
-
- // Deliver hover enter with a delay to have a change to detect
- // whether the user actually starts a scrolling gesture.
- if (lastAction == MotionEvent.ACTION_HOVER_EXIT) {
- mSendHoverDelayed.post(event, MotionEvent.ACTION_HOVER_ENTER,
- pointerIdBits, policyFlags, DELAY_SEND_HOVER_ENTER);
- } else {
- sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits,
- policyFlags);
- }
-
- if (mLastTouchExploreEvent == null) {
- break;
- }
-
- // If more pointers down on the screen since the last touch
- // exploration we discard the last cached touch explore event.
- if (event.getPointerCount() != mLastTouchExploreEvent.getPointerCount()) {
- mLastTouchExploreEvent = null;
- break;
- }
-
- // If the down is in the time slop => schedule a long press.
- final long pointerDownTime =
- receivedTracker.getReceivedPointerDownTime(pointerId);
- final long lastExploreTime = mLastTouchExploreEvent.getEventTime();
- final long deltaTimeExplore = pointerDownTime - lastExploreTime;
- if (deltaTimeExplore <= ACTIVATION_TIME_SLOP) {
- mPerformLongPressDelayed.post(event, policyFlags,
- ViewConfiguration.getLongPressTimeout());
- break;
- }
+ mSendHoverEnterDelayed.post(event, pointerIdBits, policyFlags);
} break;
default: {
/* do nothing - let the code for ACTION_MOVE decide what to do */
@@ -288,119 +361,130 @@
/* do nothing - no active pointers so we swallow the event */
} break;
case 1: {
- // Detect touch exploration gesture start by having one active pointer
- // that moved more than a given distance.
- if (!mTouchExploreGestureInProgress) {
+ // We have not started sending events since we try to
+ // figure out what the user is doing.
+ if (mSendHoverEnterDelayed.isPending()) {
+ // Pre-feed the motion events to the gesture detector since we
+ // have a distance slop before getting into gesture detection
+ // mode and not using the points within this slop significantly
+ // decreases the quality of gesture recognition.
+ handleMotionEventGestureDetecting(event, policyFlags);
+
final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId)
- event.getX(pointerIndex);
final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId)
- event.getY(pointerIndex);
final double moveDelta = Math.hypot(deltaX, deltaY);
-
- if (moveDelta > mTouchExplorationTapSlop) {
-
+ // The user has moved enough for us to decide.
+ if (moveDelta > mDoubleTapSlop) {
+ // Check whether the user is performing a gesture. We
+ // detect gestures if the pointer is moving above a
+ // given velocity.
mVelocityTracker.computeCurrentVelocity(1000);
final float maxAbsVelocity = Math.max(
Math.abs(mVelocityTracker.getXVelocity(pointerId)),
Math.abs(mVelocityTracker.getYVelocity(pointerId)));
- // TODO: Tune the velocity cut off and add a constant.
- if (maxAbsVelocity > 1000) {
- clear(event, policyFlags);
+ if (maxAbsVelocity > mScaledGestureDetectionVelocity) {
+ // We have to perform gesture detection, so
+ // clear the current state and try to detect.
mCurrentState = STATE_GESTURE_DETECTING;
- event.setAction(MotionEvent.ACTION_DOWN);
- handleMotionEventGestureDetecting(event, policyFlags);
- return;
- }
-
- mTouchExploreGestureInProgress = true;
- sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_START);
- // Make sure the scheduled down/move event is sent.
- mSendHoverDelayed.forceSendAndRemove();
- mPerformLongPressDelayed.remove();
- // If we have transitioned to exploring state from another one
- // we need to send a hover enter event here.
- final int lastAction = injectedTracker.getLastInjectedHoverAction();
- if (lastAction == MotionEvent.ACTION_HOVER_EXIT) {
- sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER,
+ mSendHoverEnterDelayed.remove();
+ mSendHoverExitDelayed.remove();
+ mPerformLongPressDelayed.remove();
+ } else {
+ // We have just decided that the user is touch,
+ // exploring so start sending events.
+ mSendHoverEnterDelayed.forceSendAndRemove();
+ mSendHoverExitDelayed.remove();
+ sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE,
pointerIdBits, policyFlags);
}
- sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits,
- policyFlags);
+ break;
}
} else {
- // Touch exploration gesture in progress so send a hover event.
+ // The user is wither double tapping or performing long
+ // press so do not send move events yet.
+ if (mDoubleTapDetector.firstTapDetected()) {
+ break;
+ }
+ sendEnterEventsIfNeeded(policyFlags);
sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits,
policyFlags);
}
-
- // If the exploring pointer moved enough => cancel the long press.
- if (!mTouchExploreGestureInProgress && mLastTouchExploreEvent != null
- && mPerformLongPressDelayed.isPenidng()) {
-
- // If the pointer moved more than the tap slop => cancel long press.
- final float deltaX = mLastTouchExploreEvent.getX(pointerIndex)
- - event.getX(pointerIndex);
- final float deltaY = mLastTouchExploreEvent.getY(pointerIndex)
- - event.getY(pointerIndex);
- final float moveDelta = (float) Math.hypot(deltaX, deltaY);
- if (moveDelta > mTouchExplorationTapSlop) {
- mLastTouchExploreEvent = null;
- mPerformLongPressDelayed.remove();
- break;
- }
- }
} break;
case 2: {
- mSendHoverDelayed.remove();
- mPerformLongPressDelayed.remove();
- // We want to no longer hover over the location so subsequent
- // touch at the same spot will generate a hover enter.
- ensureHoverExitSent(event, pointerIdBits, policyFlags);
+ // More than one pointer so the user is not touch exploring
+ // and now we have to decide whether to delegate or drag.
+ if (mSendHoverEnterDelayed.isPending()) {
+ // We have not started sending events so cancel
+ // scheduled sending events.
+ mSendHoverEnterDelayed.remove();
+ mSendHoverExitDelayed.remove();
+ mPerformLongPressDelayed.remove();
+ } else {
+ // If the user is touch exploring the second pointer may be
+ // performing a double tap to activate an item without need
+ // for the user to lift his exploring finger.
+ final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId)
+ - event.getX(pointerIndex);
+ final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId)
+ - event.getY(pointerIndex);
+ final double moveDelta = Math.hypot(deltaX, deltaY);
+ if (moveDelta < mDoubleTapSlop) {
+ break;
+ }
+ // We are sending events so send exit and gesture
+ // end since we transition to another state.
+ sendExitEventsIfNeeded(policyFlags);
+ }
+
+ // We know that a new state transition is to happen and the
+ // new state will not be gesture recognition, so clear the
+ // stashed gesture strokes.
+ mStrokeBuffer.clear();
if (isDraggingGesture(event)) {
// Two pointers moving in the same direction within
// a given distance perform a drag.
+ mSendHoverEnterDelayed.remove();
+ mSendHoverExitDelayed.remove();
+ mPerformLongPressDelayed.remove();
mCurrentState = STATE_DRAGGING;
- if (mTouchExploreGestureInProgress) {
- sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
- mTouchExploreGestureInProgress = false;
- }
- mLastTouchExploreEvent = null;
mDraggingPointerId = pointerId;
sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits,
policyFlags);
} else {
// Two pointers moving arbitrary are delegated to the view hierarchy.
mCurrentState = STATE_DELEGATING;
- mSendHoverDelayed.remove();
- if (mTouchExploreGestureInProgress) {
- sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
- mTouchExploreGestureInProgress = false;
- }
- mLastTouchExploreEvent = null;
sendDownForAllActiveNotInjectedPointers(event, policyFlags);
}
} break;
default: {
- mSendHoverDelayed.remove();
- mPerformLongPressDelayed.remove();
- // We want to no longer hover over the location so subsequent
- // touch at the same spot will generate a hover enter.
- ensureHoverExitSent(event, pointerIdBits, policyFlags);
+ // More than one pointer so the user is not touch exploring
+ // and now we have to decide whether to delegate or drag.
+ if (mSendHoverEnterDelayed.isPending()) {
+ // We have not started sending events so cancel
+ // scheduled sending events.
+ mSendHoverEnterDelayed.remove();
+ mSendHoverExitDelayed.remove();
+ mPerformLongPressDelayed.remove();
+ } else {
+ // We are sending events so send exit and gesture
+ // end since we transition to another state.
+ sendExitEventsIfNeeded(policyFlags);
+ }
// More than two pointers are delegated to the view hierarchy.
mCurrentState = STATE_DELEGATING;
- mSendHoverDelayed.remove();
- if (mTouchExploreGestureInProgress) {
- sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
- mTouchExploreGestureInProgress = false;
- }
- mLastTouchExploreEvent = null;
sendDownForAllActiveNotInjectedPointers(event, policyFlags);
}
}
} break;
case MotionEvent.ACTION_UP:
+ // We know that we do not need the pre-fed gesture points are not
+ // needed anymore since the last pointer just went up.
+ mStrokeBuffer.clear();
+ //$FALL-THROUGH$
case MotionEvent.ACTION_POINTER_UP: {
final int pointerId = receivedTracker.getLastReceivedUpPointerId();
final int pointerIdBits = (1 << pointerId);
@@ -413,59 +497,12 @@
mPerformLongPressDelayed.remove();
- // If touch exploring announce the end of the gesture.
- // Also do not click on the last explored location.
- if (mTouchExploreGestureInProgress) {
- mTouchExploreGestureInProgress = false;
- mSendHoverDelayed.forceSendAndRemove();
- ensureHoverExitSent(event, pointerIdBits, policyFlags);
- mLastTouchExploreEvent = MotionEvent.obtain(event);
- sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END);
- break;
- }
-
- // Detect whether to activate i.e. click on the last explored location.
- if (mLastTouchExploreEvent != null) {
- // If the down was not in the time slop => nothing else to do.
- final long eventTime =
- receivedTracker.getLastReceivedUpPointerDownTime();
- final long exploreTime = mLastTouchExploreEvent.getEventTime();
- final long deltaTime = eventTime - exploreTime;
- if (deltaTime > ACTIVATION_TIME_SLOP) {
- mSendHoverDelayed.forceSendAndRemove();
- ensureHoverExitSent(event, pointerIdBits, policyFlags);
- mLastTouchExploreEvent = MotionEvent.obtain(event);
- break;
- }
-
- // If a tap is farther than the tap slop => nothing to do.
- final int pointerIndex = event.findPointerIndex(pointerId);
- final float deltaX = mLastTouchExploreEvent.getX(pointerIndex)
- - event.getX(pointerIndex);
- final float deltaY = mLastTouchExploreEvent.getY(pointerIndex)
- - event.getY(pointerIndex);
- final float deltaMove = (float) Math.hypot(deltaX, deltaY);
- if (deltaMove > mTouchExplorationTapSlop) {
- mSendHoverDelayed.forceSendAndRemove();
- ensureHoverExitSent(event, pointerIdBits, policyFlags);
- mLastTouchExploreEvent = MotionEvent.obtain(event);
- break;
- }
-
- // This is a tap so do not send hover events since
- // this events will result in firing the corresponding
- // accessibility events confusing the user about what
- // is actually clicked.
- mSendHoverDelayed.remove();
- ensureHoverExitSent(event, pointerIdBits, policyFlags);
-
- // All preconditions are met, so click the last explored location.
- sendActionDownAndUp(mLastTouchExploreEvent, policyFlags);
- mLastTouchExploreEvent = null;
+ // If we have not delivered the enter schedule exit.
+ if (mSendHoverEnterDelayed.isPending()) {
+ mSendHoverExitDelayed.post(event, pointerIdBits, policyFlags);
} else {
- mSendHoverDelayed.forceSendAndRemove();
- ensureHoverExitSent(event, pointerIdBits, policyFlags);
- mLastTouchExploreEvent = MotionEvent.obtain(event);
+ // The user is touch exploring so we send events for end.
+ sendExitEventsIfNeeded(policyFlags);
}
} break;
}
@@ -475,16 +512,7 @@
}
} break;
case MotionEvent.ACTION_CANCEL: {
- mSendHoverDelayed.remove();
- mPerformLongPressDelayed.remove();
- final int pointerId = receivedTracker.getPrimaryActivePointerId();
- final int pointerIdBits = (1 << pointerId);
- ensureHoverExitSent(event, pointerIdBits, policyFlags);
- clear();
- if (mVelocityTracker != null) {
- mVelocityTracker.clear();
- mVelocityTracker = null;
- }
+ clear(event, policyFlags);
} break;
}
}
@@ -517,6 +545,28 @@
} break;
case 2: {
if (isDraggingGesture(event)) {
+ // If the dragging pointer are closer that a given distance we
+ // use the location of the primary one. Otherwise, we take the
+ // middle between the pointers.
+ int[] pointerIds = mTempPointerIds;
+ mReceivedPointerTracker.populateActivePointerIds(pointerIds);
+
+ final int firstPtrIndex = event.findPointerIndex(pointerIds[0]);
+ final int secondPtrIndex = event.findPointerIndex(pointerIds[1]);
+
+ final float firstPtrX = event.getX(firstPtrIndex);
+ final float firstPtrY = event.getY(firstPtrIndex);
+ final float secondPtrX = event.getX(secondPtrIndex);
+ final float secondPtrY = event.getY(secondPtrIndex);
+
+ final float deltaX = firstPtrX - secondPtrX;
+ final float deltaY = firstPtrY - secondPtrY;
+ final double distance = Math.hypot(deltaX, deltaY);
+
+ if (distance > mScaledMinPointerDistanceToUseMiddleLocation) {
+ event.setLocation(deltaX / 2, deltaY / 2);
+ }
+
// If still dragging send a drag event.
sendMotionEvent(event, MotionEvent.ACTION_MOVE, pointerIdBits,
policyFlags);
@@ -557,7 +607,7 @@
mCurrentState = STATE_TOUCH_EXPLORING;
} break;
case MotionEvent.ACTION_CANCEL: {
- clear();
+ clear(event, policyFlags);
} break;
}
}
@@ -574,9 +624,6 @@
throw new IllegalStateException("Delegating state can only be reached if "
+ "there is at least one pointer down!");
}
- case MotionEvent.ACTION_UP: {
- mCurrentState = STATE_TOUCH_EXPLORING;
- } break;
case MotionEvent.ACTION_MOVE: {
// Check whether some other pointer became active because they have moved
// a given distance and if such exist send them to the view hierarchy
@@ -587,30 +634,24 @@
sendDownForAllActiveNotInjectedPointers(prototype, policyFlags);
}
} break;
+ case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
+ mLongPressingPointerId = -1;
+ mLongPressingPointerDeltaX = 0;
+ mLongPressingPointerDeltaY = 0;
// No active pointers => go to initial state.
if (mReceivedPointerTracker.getActivePointerCount() == 0) {
mCurrentState = STATE_TOUCH_EXPLORING;
}
} break;
case MotionEvent.ACTION_CANCEL: {
- clear();
+ clear(event, policyFlags);
} break;
}
// Deliver the event striping out inactive pointers.
sendMotionEventStripInactivePointers(event, policyFlags);
}
- private float mPreviousX;
- private float mPreviousY;
-
- private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);
-
- private static final int TOUCH_TOLERANCE = 3;
- private static final float MIN_PREDICTION_SCORE = 2.0f;
-
- private GestureLibrary mGestureLibrary;
-
private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
@@ -631,8 +672,7 @@
mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
}
} break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_POINTER_UP: {
+ case MotionEvent.ACTION_UP: {
float x = event.getX();
float y = event.getY();
mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
@@ -650,7 +690,7 @@
}
try {
final int gestureId = Integer.parseInt(bestPrediction.name);
- mGestureListener.onGesture(gestureId);
+ mAms.onGesture(gestureId);
} catch (NumberFormatException nfe) {
Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name);
}
@@ -661,8 +701,7 @@
mCurrentState = STATE_TOUCH_EXPLORING;
} break;
case MotionEvent.ACTION_CANCEL: {
- mStrokeBuffer.clear();
- mCurrentState = STATE_TOUCH_EXPLORING;
+ clear(event, policyFlags);
} break;
}
}
@@ -706,17 +745,32 @@
}
/**
- * Ensures that hover exit has been sent.
+ * Sends the exit events if needed. Such events are hover exit and touch explore
+ * gesture end.
*
- * @param prototype The prototype from which to create the injected events.
- * @param pointerIdBits The bits of the pointers to send.
* @param policyFlags The policy flags associated with the event.
*/
- private void ensureHoverExitSent(MotionEvent prototype, int pointerIdBits, int policyFlags) {
- final int lastAction = mInjectedPointerTracker.getLastInjectedHoverAction();
- if (lastAction != MotionEvent.ACTION_HOVER_EXIT) {
- sendMotionEvent(prototype, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits,
- policyFlags);
+ private void sendExitEventsIfNeeded(int policyFlags) {
+ MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent();
+ if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) {
+ final int pointerIdBits = event.getPointerIdBits();
+ mAms.touchExplorationGestureEnded();
+ sendMotionEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags);
+ }
+ }
+
+ /**
+ * Sends the enter events if needed. Such events are hover enter and touch explore
+ * gesture start.
+ *
+ * @param policyFlags The policy flags associated with the event.
+ */
+ private void sendEnterEventsIfNeeded(int policyFlags) {
+ MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent();
+ if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
+ final int pointerIdBits = event.getPointerIdBits();
+ mAms.touchExplorationGestureStarted();
+ sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags);
}
}
@@ -826,6 +880,36 @@
event.setDownTime(mInjectedPointerTracker.getLastInjectedDownEventTime());
}
+ // If the user is long pressing but the long pressing pointer
+ // was not exactly over the accessibility focused item we need
+ // to remap the location of that pointer so the user does not
+ // have to explicitly touch explore something to be able to
+ // long press it, or even worse to avoid the user long pressing
+ // on the wrong item since click and long press behave differently.
+ if (mLongPressingPointerId >= 0) {
+ final int remappedIndex = event.findPointerIndex(mLongPressingPointerId);
+ final int pointerCount = event.getPointerCount();
+ PointerProperties[] props = PointerProperties.createArray(pointerCount);
+ PointerCoords[] coords = PointerCoords.createArray(pointerCount);
+ for (int i = 0; i < pointerCount; i++) {
+ event.getPointerProperties(i, props[i]);
+ event.getPointerCoords(i, coords[i]);
+ if (i == remappedIndex) {
+ coords[i].x -= mLongPressingPointerDeltaX;
+ coords[i].y -= mLongPressingPointerDeltaY;
+ }
+ }
+ MotionEvent remapped = MotionEvent.obtain(event.getDownTime(),
+ event.getEventTime(), event.getAction(), event.getPointerCount(),
+ props, coords, event.getMetaState(), event.getButtonState(),
+ 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(),
+ event.getSource(), event.getFlags());
+ if (event != prototype) {
+ event.recycle();
+ }
+ event = remapped;
+ }
+
if (DEBUG) {
Slog.d(LOG_TAG, "Injecting event: " + event + ", policyFlags=0x"
+ Integer.toHexString(policyFlags));
@@ -878,6 +962,172 @@
}
}
+ private class DoubleTapDetector {
+ private MotionEvent mDownEvent;
+ private MotionEvent mFirstTapEvent;
+
+ public void onMotionEvent(MotionEvent event, int policyFlags) {
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ if (mFirstTapEvent != null && !isSamePointerContext(mFirstTapEvent, event)) {
+ clear();
+ }
+ mDownEvent = MotionEvent.obtain(event);
+ } break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP: {
+ if (mDownEvent == null) {
+ return;
+ }
+ if (!isSamePointerContext(mDownEvent, event)) {
+ clear();
+ return;
+ }
+ if (isTap(mDownEvent, event)) {
+ if (mFirstTapEvent == null || isTimedOut(mFirstTapEvent, event,
+ mDoubleTapTimeout)) {
+ mFirstTapEvent = MotionEvent.obtain(event);
+ mDownEvent.recycle();
+ mDownEvent = null;
+ return;
+ }
+ if (isDoubleTap(mFirstTapEvent, event)) {
+ onDoubleTap(event, policyFlags);
+ mFirstTapEvent.recycle();
+ mFirstTapEvent = null;
+ mDownEvent.recycle();
+ mDownEvent = null;
+ return;
+ }
+ mFirstTapEvent.recycle();
+ mFirstTapEvent = null;
+ } else {
+ if (mFirstTapEvent != null) {
+ mFirstTapEvent.recycle();
+ mFirstTapEvent = null;
+ }
+ }
+ mDownEvent.recycle();
+ mDownEvent = null;
+ } break;
+ }
+ }
+
+ public void onDoubleTap(MotionEvent secondTapUp, int policyFlags) {
+ // This should never be called when more than two pointers are down.
+ if (secondTapUp.getPointerCount() > 2) {
+ return;
+ }
+
+ // Remove pending event deliveries.
+ mSendHoverEnterDelayed.remove();
+ mSendHoverExitDelayed.remove();
+ mPerformLongPressDelayed.remove();
+
+ // This is a tap so do not send hover events since
+ // this events will result in firing the corresponding
+ // accessibility events confusing the user about what
+ // is actually clicked.
+ sendExitEventsIfNeeded(policyFlags);
+
+ // If the last touched explored location is not within the focused
+ // window we will click at that exact spot, otherwise we find the
+ // accessibility focus and if the tap is within its bounds we click
+ // there, otherwise we pick the middle of the focus rectangle.
+ MotionEvent lastEvent = mInjectedPointerTracker.getLastInjectedHoverEvent();
+ if (lastEvent == null) {
+ return;
+ }
+
+ final int exploreLocationX = (int) lastEvent.getX(lastEvent.getActionIndex());
+ final int exploreLocationY = (int) lastEvent.getY(lastEvent.getActionIndex());
+
+ Rect bounds = mTempRect;
+ boolean useLastHoverLocation = false;
+
+ final int pointerId = secondTapUp.getPointerId(secondTapUp.getActionIndex());
+ final int pointerIndex = secondTapUp.findPointerIndex(pointerId);
+ if (mAms.getAccessibilityFocusBounds(exploreLocationX, exploreLocationY, bounds)) {
+ // If the user's last touch explored location is not
+ // within the accessibility focus bounds we use the center
+ // of the accessibility focused rectangle.
+ if (!bounds.contains((int) secondTapUp.getX(pointerIndex),
+ (int) secondTapUp.getY(pointerIndex))) {
+ useLastHoverLocation = true;
+ }
+ }
+
+ // Do the click.
+ PointerProperties[] properties = new PointerProperties[1];
+ properties[0] = new PointerProperties();
+ secondTapUp.getPointerProperties(pointerIndex, properties[0]);
+ PointerCoords[] coords = new PointerCoords[1];
+ coords[0] = new PointerCoords();
+ coords[0].x = (useLastHoverLocation) ? bounds.centerX() : exploreLocationX;
+ coords[0].y = (useLastHoverLocation) ? bounds.centerY() : exploreLocationY;
+ MotionEvent event = MotionEvent.obtain(secondTapUp.getDownTime(),
+ secondTapUp.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties,
+ coords, 0, 0, 1.0f, 1.0f, secondTapUp.getDeviceId(), 0,
+ secondTapUp.getSource(), secondTapUp.getFlags());
+ sendActionDownAndUp(event, policyFlags);
+ event.recycle();
+ }
+
+ public void clear() {
+ if (mDownEvent != null) {
+ mDownEvent.recycle();
+ mDownEvent = null;
+ }
+ if (mFirstTapEvent != null) {
+ mFirstTapEvent.recycle();
+ mFirstTapEvent = null;
+ }
+ }
+
+ public boolean isTap(MotionEvent down, MotionEvent up) {
+ return eventsWithinTimeoutAndDistance(down, up, mTapTimeout, mTouchSlop);
+ }
+
+ private boolean isDoubleTap(MotionEvent firstUp, MotionEvent secondUp) {
+ return eventsWithinTimeoutAndDistance(firstUp, secondUp, mDoubleTapTimeout,
+ mDoubleTapSlop);
+ }
+
+ private boolean eventsWithinTimeoutAndDistance(MotionEvent first, MotionEvent second,
+ int timeout, int distance) {
+ if (isTimedOut(first, second, timeout)) {
+ return false;
+ }
+ final int downPtrIndex = first.getActionIndex();
+ final int upPtrIndex = second.getActionIndex();
+ final float deltaX = second.getX(upPtrIndex) - first.getX(downPtrIndex);
+ final float deltaY = second.getY(upPtrIndex) - first.getY(downPtrIndex);
+ final double deltaMove = Math.hypot(deltaX, deltaY);
+ if (deltaMove >= distance) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) {
+ final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime();
+ return (deltaTime >= timeout);
+ }
+
+ private boolean isSamePointerContext(MotionEvent first, MotionEvent second) {
+ return (first.getPointerIdBits() == second.getPointerIdBits()
+ && first.getPointerId(first.getActionIndex())
+ == second.getPointerId(second.getActionIndex()));
+ }
+
+ public boolean firstTapDetected() {
+ return mFirstTapEvent != null
+ && SystemClock.uptimeMillis() - mFirstTapEvent.getEventTime() < mDoubleTapTimeout;
+ }
+ }
+
/**
* Determines whether a two pointer gesture is a dragging one.
*
@@ -940,30 +1190,6 @@
return true;
}
- /**
- * Sends an event announcing the start/end of a touch exploration gesture.
- *
- * @param eventType The type of the event to send.
- */
- private void sendAccessibilityEvent(int eventType) {
- AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
- mAccessibilityManager.sendAccessibilityEvent(event);
- }
-
- /**
- * Clears the internal state of this explorer.
- */
- public void clear() {
- mSendHoverDelayed.remove();
- mPerformLongPressDelayed.remove();
- mReceivedPointerTracker.clear();
- mInjectedPointerTracker.clear();
- mLastTouchExploreEvent = null;
- mCurrentState = STATE_TOUCH_EXPLORING;
- mTouchExploreGestureInProgress = false;
- mDraggingPointerId = INVALID_POINTER_ID;
- }
-
/**
* Gets the symbolic name of a state.
*
@@ -1002,10 +1228,10 @@
private MotionEvent mEvent;
private int mPolicyFlags;
- public void post(MotionEvent prototype, int policyFlags, long delay) {
+ public void post(MotionEvent prototype, int policyFlags) {
mEvent = MotionEvent.obtain(prototype);
mPolicyFlags = policyFlags;
- mHandler.postDelayed(this, delay);
+ mHandler.postDelayed(this, ViewConfiguration.getLongPressTimeout());
}
public void remove() {
@@ -1021,16 +1247,29 @@
@Override
public void run() {
- mCurrentState = STATE_DELEGATING;
- // Make sure the scheduled hover exit is delivered.
- mSendHoverDelayed.remove();
+ final int pointerIndex = mEvent.getActionIndex();
+ final int eventX = (int) mEvent.getX(pointerIndex);
+ final int eventY = (int) mEvent.getY(pointerIndex);
+ Rect bounds = mTempRect;
+ if (mAms.getAccessibilityFocusBounds(eventX, eventY, bounds)
+ && !bounds.contains(eventX, eventY)) {
+ mLongPressingPointerId = mEvent.getPointerId(pointerIndex);
+ mLongPressingPointerDeltaX = eventX - bounds.centerX();
+ mLongPressingPointerDeltaY = eventY - bounds.centerY();
+ } else {
+ mLongPressingPointerId = -1;
+ mLongPressingPointerDeltaX = 0;
+ mLongPressingPointerDeltaY = 0;
+ }
+ // We are sending events so send exit and gesture
+ // end since we transition to another state.
final int pointerId = mReceivedPointerTracker.getPrimaryActivePointerId();
final int pointerIdBits = (1 << pointerId);
- ensureHoverExitSent(mEvent, pointerIdBits, mPolicyFlags);
+ mAms.touchExplorationGestureEnded();
+ sendExitEventsIfNeeded(mPolicyFlags);
+ mCurrentState = STATE_DELEGATING;
sendDownForAllActiveNotInjectedPointers(mEvent, mPolicyFlags);
- mTouchExploreGestureInProgress = false;
- mLastTouchExploreEvent = null;
clear();
}
@@ -1047,20 +1286,41 @@
/**
* Class for delayed sending of hover events.
*/
- private final class SendHoverDelayed implements Runnable {
- private MotionEvent mEvent;
- private int mAction;
+ class SendHoverDelayed implements Runnable {
+ private final String LOG_TAG_SEND_HOVER_DELAYED = SendHoverDelayed.class.getName();
+
+ private final int mHoverAction;
+ private final boolean mGestureStarted;
+
+ private MotionEvent mPrototype;
private int mPointerIdBits;
private int mPolicyFlags;
- public void post(MotionEvent prototype, int action, int pointerIdBits, int policyFlags,
- long delay) {
+ public SendHoverDelayed(int hoverAction, boolean gestureStarted) {
+ mHoverAction = hoverAction;
+ mGestureStarted = gestureStarted;
+ }
+
+ public void post(MotionEvent prototype, int pointerIdBits, int policyFlags) {
remove();
- mEvent = MotionEvent.obtain(prototype);
- mAction = action;
+ mPrototype = MotionEvent.obtain(prototype);
mPointerIdBits = pointerIdBits;
mPolicyFlags = policyFlags;
- mHandler.postDelayed(this, delay);
+ mHandler.postDelayed(this, mTapTimeout);
+ }
+
+ public float getX() {
+ if (isPending()) {
+ return mPrototype.getX();
+ }
+ return 0;
+ }
+
+ public float getY() {
+ if (isPending()) {
+ return mPrototype.getY();
+ }
+ return 0;
}
public void remove() {
@@ -1068,23 +1328,22 @@
clear();
}
- private boolean isPenidng() {
- return (mEvent != null);
+ private boolean isPending() {
+ return (mPrototype != null);
}
private void clear() {
- if (!isPenidng()) {
+ if (!isPending()) {
return;
}
- mEvent.recycle();
- mEvent = null;
- mAction = 0;
+ mPrototype.recycle();
+ mPrototype = null;
mPointerIdBits = -1;
mPolicyFlags = 0;
}
public void forceSendAndRemove() {
- if (isPenidng()) {
+ if (isPending()) {
run();
remove();
}
@@ -1092,16 +1351,17 @@
public void run() {
if (DEBUG) {
- if (mAction == MotionEvent.ACTION_HOVER_ENTER) {
- Slog.d(LOG_TAG, "Injecting: " + MotionEvent.ACTION_HOVER_ENTER);
- } else if (mAction == MotionEvent.ACTION_HOVER_MOVE) {
- Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_MOVE");
- } else if (mAction == MotionEvent.ACTION_HOVER_EXIT) {
- Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_EXIT");
- }
+ Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event: "
+ + MotionEvent.actionToString(mHoverAction));
+ Slog.d(LOG_TAG_SEND_HOVER_DELAYED, mGestureStarted ?
+ "touchExplorationGestureStarted" : "touchExplorationGestureEnded");
}
-
- sendMotionEvent(mEvent, mAction, mPointerIdBits, mPolicyFlags);
+ if (mGestureStarted) {
+ mAms.touchExplorationGestureStarted();
+ } else {
+ mAms.touchExplorationGestureEnded();
+ }
+ sendMotionEvent(mPrototype, mHoverAction, mPointerIdBits, mPolicyFlags);
clear();
}
}
@@ -1120,8 +1380,8 @@
// The time of the last injected down.
private long mLastInjectedDownEventTime;
- // The action of the last injected hover event.
- private int mLastInjectedHoverEventAction = MotionEvent.ACTION_HOVER_EXIT;
+ // The last injected hover event.
+ private MotionEvent mLastInjectedHoverEvent;
/**
* Processes an injected {@link MotionEvent} event.
@@ -1150,11 +1410,14 @@
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_HOVER_EXIT: {
- mLastInjectedHoverEventAction = event.getActionMasked();
+ if (mLastInjectedHoverEvent != null) {
+ mLastInjectedHoverEvent.recycle();
+ }
+ mLastInjectedHoverEvent = MotionEvent.obtain(event);
} break;
}
if (DEBUG) {
- Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer: " + toString());
+ Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer:\n" + toString());
}
}
@@ -1198,10 +1461,10 @@
}
/**
- * @return The action of the last injected hover event.
+ * @return The the last injected hover event.
*/
- public int getLastInjectedHoverAction() {
- return mLastInjectedHoverEventAction;
+ public MotionEvent getLastInjectedHoverEvent() {
+ return mLastInjectedHoverEvent;
}
@Override
@@ -1260,6 +1523,8 @@
private float mLastReceivedUpPointerDownX;
private float mLastReceivedUpPointerDownY;
+ private MotionEvent mLastReceivedEvent;
+
/**
* Creates a new instance.
*
@@ -1294,6 +1559,11 @@
* @param event The event to process.
*/
public void onMotionEvent(MotionEvent event) {
+ if (mLastReceivedEvent != null) {
+ mLastReceivedEvent.recycle();
+ }
+ mLastReceivedEvent = MotionEvent.obtain(event);
+
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN: {
@@ -1318,6 +1588,13 @@
}
/**
+ * @return The last received event.
+ */
+ public MotionEvent getLastReceivedEvent() {
+ return mLastReceivedEvent;
+ }
+
+ /**
* @return The number of received pointers that are down.
*/
public int getReceivedPointerDownCount() {
diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java
index 885ec96..fbf9256 100755
--- a/services/java/com/android/server/wm/WindowManagerService.java
+++ b/services/java/com/android/server/wm/WindowManagerService.java
@@ -6567,6 +6567,16 @@
sendScreenStatusToClients();
}
+ public IBinder getFocusedWindowClientToken() {
+ synchronized (mWindowMap) {
+ WindowState windowState = getFocusedWindowLocked();
+ if (windowState != null) {
+ return windowState.mClient.asBinder();
+ }
+ return null;
+ }
+ }
+
private WindowState getFocusedWindow() {
synchronized (mWindowMap) {
return getFocusedWindowLocked();