blob: 7569b05abaac5211ebb78677a28ef3be648afbde [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.accessibility;
import static android.view.MotionEvent.INVALID_POINTER_ID;
import android.annotation.IntDef;
import android.util.Slog;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
/**
* This class describes the state of the touch explorer as well as the state of received and
* injected pointers. This data is accessed both for purposes of touch exploration and gesture
* dispatch.
*/
public class TouchState {
private static final boolean DEBUG = false;
private static final String LOG_TAG = "TouchState";
// Pointer-related constants
// 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)
private static final int MAX_POINTER_COUNT = 32;
// Constant referring to the ids bits of all pointers.
public static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF;
// States that the touch explorer can be in.
public static final int STATE_TOUCH_EXPLORING = 0x00000001;
public static final int STATE_DRAGGING = 0x00000002;
public static final int STATE_DELEGATING = 0x00000003;
public static final int STATE_GESTURE_DETECTING = 0x00000004;
@IntDef({STATE_TOUCH_EXPLORING, STATE_DRAGGING, STATE_DELEGATING, STATE_GESTURE_DETECTING})
public @interface State {}
// The current state of the touch explorer.
private int mState = STATE_TOUCH_EXPLORING;
// Whether touch exploration is in progress.
// TODO: Add separate states to represent intend detection and actual touch exploration so that
// only one variable describes the state.
private boolean mTouchExplorationInProgress;
// Helper class to track received pointers.
// Todo: collapse or hide this class so multiple classes don't modify it.
private final ReceivedPointerTracker mReceivedPointerTracker;
// Helper class to track injected pointers.
// Todo: collapse or hide this class so multiple classes don't modify it.
private final InjectedPointerTracker mInjectedPointerTracker;
public TouchState() {
mReceivedPointerTracker = new ReceivedPointerTracker();
mInjectedPointerTracker = new InjectedPointerTracker();
}
/** Clears the internal shared state. */
public void clear() {
mState = STATE_TOUCH_EXPLORING;
mTouchExplorationInProgress = false;
// Reset the pointer trackers.
mReceivedPointerTracker.clear();
mInjectedPointerTracker.clear();
}
/**
* Updates the state in response to a hover event dispatched by TouchExplorer.
*
* @param event The event sent from TouchExplorer.
*/
public void onInjectedMotionEvent(MotionEvent event) {
mInjectedPointerTracker.onMotionEvent(event);
}
/**
* Updates the state in response to a touch event received by TouchExplorer.
*
* @param rawEvent The raw touch event.
*/
public void onReceivedMotionEvent(MotionEvent rawEvent) {
mReceivedPointerTracker.onMotionEvent(rawEvent);
}
/**
* Updates the state in response to an accessibility event being sent from TouchExplorer.
*
* @param type The event type.
*/
public void onInjectedAccessibilityEvent(int type) {
switch (type) {
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
mTouchExplorationInProgress = true;
break;
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
mTouchExplorationInProgress = false;
break;
}
}
@State
public int getState() {
return mState;
}
/** Transitions to a new state. */
public void setState(@State int state) {
if (DEBUG) {
Slog.i(LOG_TAG, getStateSymbolicName(mState) + "->" + getStateSymbolicName(state));
}
mState = state;
}
public boolean isTouchExploring() {
return mState == STATE_TOUCH_EXPLORING;
}
/** Starts touch exploration. */
public void startTouchExploring() {
setState(STATE_TOUCH_EXPLORING);
}
public boolean isDelegating() {
return mState == STATE_DELEGATING;
}
/** Starts delegating gestures to the view hierarchy. */
public void startDelegating() {
setState(STATE_DELEGATING);
}
public boolean isGestureDetecting() {
return mState == STATE_GESTURE_DETECTING;
}
/** Initiates gesture detection. */
public void startGestureDetecting() {
setState(STATE_GESTURE_DETECTING);
}
public boolean isDragging() {
return mState == STATE_DRAGGING;
}
/** Starts a dragging gesture. */
public void startDragging() {
setState(STATE_DRAGGING);
}
public boolean isTouchExplorationInProgress() {
return mTouchExplorationInProgress;
}
public void setTouchExplorationInProgress(boolean touchExplorationInProgress) {
mTouchExplorationInProgress = touchExplorationInProgress;
}
/** Returns a string representation of the current state. */
public String toString() {
return "TouchState { "
+ "mState: "
+ getStateSymbolicName(mState)
+ ", mTouchExplorationInProgress"
+ mTouchExplorationInProgress
+ " }";
}
/** Returns a string representation of the specified state. */
public static String getStateSymbolicName(int state) {
switch (state) {
case STATE_TOUCH_EXPLORING:
return "STATE_TOUCH_EXPLORING";
case STATE_DRAGGING:
return "STATE_DRAGGING";
case STATE_DELEGATING:
return "STATE_DELEGATING";
case STATE_GESTURE_DETECTING:
return "STATE_GESTURE_DETECTING";
default:
return "Unknown state: " + state;
}
}
public InjectedPointerTracker getInjectedPointerTracker() {
return mInjectedPointerTracker;
}
public ReceivedPointerTracker getReceivedPointerTracker() {
return mReceivedPointerTracker;
}
/** This class tracks the up/down state of each pointer. It does not track movement. */
class InjectedPointerTracker {
private static final String LOG_TAG_INJECTED_POINTER_TRACKER = "InjectedPointerTracker";
// Keep track of which pointers sent to the system are down.
private int mInjectedPointersDown;
// The time of the last injected down.
private long mLastInjectedDownEventTime;
// The last injected hover event.
private MotionEvent mLastInjectedHoverEvent;
/**
* Processes an injected {@link MotionEvent} event.
*
* @param event The event to process.
*/
public void onMotionEvent(MotionEvent event) {
final int action = event.getActionMasked();
final int pointerId = event.getPointerId(event.getActionIndex());
final int pointerFlag = (1 << pointerId);
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
mInjectedPointersDown |= pointerFlag;
mLastInjectedDownEventTime = event.getDownTime();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mInjectedPointersDown &= ~pointerFlag;
if (mInjectedPointersDown == 0) {
mLastInjectedDownEventTime = 0;
}
break;
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_HOVER_EXIT:
if (mLastInjectedHoverEvent != null) {
mLastInjectedHoverEvent.recycle();
}
mLastInjectedHoverEvent = MotionEvent.obtain(event);
break;
}
if (DEBUG) {
Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer:\n" + toString());
}
}
/** Clears the internals state. */
public void clear() {
mInjectedPointersDown = 0;
}
/** @return The time of the last injected down event. */
public long getLastInjectedDownEventTime() {
return mLastInjectedDownEventTime;
}
/** @return The number of down pointers injected to the view hierarchy. */
public int getInjectedPointerDownCount() {
return Integer.bitCount(mInjectedPointersDown);
}
/** @return The bits of the injected pointers that are down. */
public int getInjectedPointersDown() {
return mInjectedPointersDown;
}
/**
* Whether an injected pointer is down.
*
* @param pointerId The unique pointer id.
* @return True if the pointer is down.
*/
public boolean isInjectedPointerDown(int pointerId) {
final int pointerFlag = (1 << pointerId);
return (mInjectedPointersDown & pointerFlag) != 0;
}
/** @return The the last injected hover event. */
public MotionEvent getLastInjectedHoverEvent() {
return mLastInjectedHoverEvent;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("=========================");
builder.append("\nDown pointers #");
builder.append(Integer.bitCount(mInjectedPointersDown));
builder.append(" [ ");
for (int i = 0; i < MAX_POINTER_COUNT; i++) {
if ((mInjectedPointersDown & i) != 0) {
builder.append(i);
builder.append(" ");
}
}
builder.append("]");
builder.append("\n=========================");
return builder.toString();
}
}
/** This class tracks where and when a pointer went down. It does not track its movement. */
class ReceivedPointerTracker {
private static final String LOG_TAG_RECEIVED_POINTER_TRACKER = "ReceivedPointerTracker";
private final PointerDownInfo[] mReceivedPointers = new PointerDownInfo[MAX_POINTER_COUNT];
// Which pointers are down.
private int mReceivedPointersDown;
// The edge flags of the last received down event.
private int mLastReceivedDownEdgeFlags;
// Primary pointer which is either the first that went down
// or if it goes up the next one that most recently went down.
private int mPrimaryPointerId;
// Keep track of the last up pointer data.
private MotionEvent mLastReceivedEvent;
ReceivedPointerTracker() {
clear();
}
/** Clears the internals state. */
public void clear() {
mReceivedPointersDown = 0;
mPrimaryPointerId = 0;
for (int i = 0; i < MAX_POINTER_COUNT; ++i) {
mReceivedPointers[i] = new PointerDownInfo();
}
}
/**
* Processes a received {@link MotionEvent} event.
*
* @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:
handleReceivedPointerDown(event.getActionIndex(), event);
break;
case MotionEvent.ACTION_POINTER_DOWN:
handleReceivedPointerDown(event.getActionIndex(), event);
break;
case MotionEvent.ACTION_UP:
handleReceivedPointerUp(event.getActionIndex(), event);
break;
case MotionEvent.ACTION_POINTER_UP:
handleReceivedPointerUp(event.getActionIndex(), event);
break;
}
if (DEBUG) {
Slog.i(LOG_TAG_RECEIVED_POINTER_TRACKER, "Received pointer:\n" + toString());
}
}
/** @return The last received event. */
public MotionEvent getLastReceivedEvent() {
return mLastReceivedEvent;
}
/** @return The number of received pointers that are down. */
public int getReceivedPointerDownCount() {
return Integer.bitCount(mReceivedPointersDown);
}
/**
* Whether an received pointer is down.
*
* @param pointerId The unique pointer id.
* @return True if the pointer is down.
*/
public boolean isReceivedPointerDown(int pointerId) {
final int pointerFlag = (1 << pointerId);
return (mReceivedPointersDown & pointerFlag) != 0;
}
/**
* @param pointerId The unique pointer id.
* @return The X coordinate where the pointer went down.
*/
public float getReceivedPointerDownX(int pointerId) {
return mReceivedPointers[pointerId].mX;
}
/**
* @param pointerId The unique pointer id.
* @return The Y coordinate where the pointer went down.
*/
public float getReceivedPointerDownY(int pointerId) {
return mReceivedPointers[pointerId].mY;
}
/**
* @param pointerId The unique pointer id.
* @return The time when the pointer went down.
*/
public long getReceivedPointerDownTime(int pointerId) {
return mReceivedPointers[pointerId].mTime;
}
/** @return The id of the primary pointer. */
public int getPrimaryPointerId() {
if (mPrimaryPointerId == INVALID_POINTER_ID) {
mPrimaryPointerId = findPrimaryPointerId();
}
return mPrimaryPointerId;
}
/** @return The edge flags of the last received down event. */
public int getLastReceivedDownEdgeFlags() {
return mLastReceivedDownEdgeFlags;
}
/**
* Handles a received pointer down event.
*
* @param pointerIndex The index of the pointer that has changed.
* @param event The event to be handled.
*/
private void handleReceivedPointerDown(int pointerIndex, MotionEvent event) {
final int pointerId = event.getPointerId(pointerIndex);
final int pointerFlag = (1 << pointerId);
mLastReceivedDownEdgeFlags = event.getEdgeFlags();
mReceivedPointersDown |= pointerFlag;
mReceivedPointers[pointerId].set(
event.getX(pointerIndex), event.getY(pointerIndex), event.getEventTime());
mPrimaryPointerId = pointerId;
}
/**
* Handles a received pointer up event.
*
* @param pointerIndex The index of the pointer that has changed.
* @param event The event to be handled.
*/
private void handleReceivedPointerUp(int pointerIndex, MotionEvent event) {
final int pointerId = event.getPointerId(pointerIndex);
final int pointerFlag = (1 << pointerId);
mReceivedPointersDown &= ~pointerFlag;
mReceivedPointers[pointerId].clear();
if (mPrimaryPointerId == pointerId) {
mPrimaryPointerId = INVALID_POINTER_ID;
}
}
/** @return The primary pointer id. */
private int findPrimaryPointerId() {
int primaryPointerId = INVALID_POINTER_ID;
long minDownTime = Long.MAX_VALUE;
// Find the pointer that went down first.
int pointerIdBits = mReceivedPointersDown;
while (pointerIdBits > 0) {
final int pointerId = Integer.numberOfTrailingZeros(pointerIdBits);
pointerIdBits &= ~(1 << pointerId);
final long downPointerTime = mReceivedPointers[pointerId].mTime;
if (downPointerTime < minDownTime) {
minDownTime = downPointerTime;
primaryPointerId = pointerId;
}
}
return primaryPointerId;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("=========================");
builder.append("\nDown pointers #");
builder.append(getReceivedPointerDownCount());
builder.append(" [ ");
for (int i = 0; i < MAX_POINTER_COUNT; i++) {
if (isReceivedPointerDown(i)) {
builder.append(i);
builder.append(" ");
}
}
builder.append("]");
builder.append("\nPrimary pointer id [ ");
builder.append(getPrimaryPointerId());
builder.append(" ]");
builder.append("\n=========================");
return builder.toString();
}
}
/**
* This class tracks where and when an individual pointer went down. Note that it does not track
* when it went up.
*/
class PointerDownInfo {
private float mX;
private float mY;
private long mTime;
public void set(float x, float y, long time) {
mX = x;
mY = y;
mTime = time;
}
public void clear() {
mX = 0;
mY = 0;
mTime = 0;
}
}
}