blob: 6014017239f029820f5500e681386645b4ebc7d3 [file] [log] [blame]
//
// Copyright 2010 The Android Open Source Project
//
// The input reader.
//
#define LOG_TAG "InputDevice"
//#define LOG_NDEBUG 0
// Log debug messages for each raw event received from the EventHub.
#define DEBUG_RAW_EVENTS 0
// Log debug messages about touch screen filtering hacks.
#define DEBUG_HACKS 0
// Log debug messages about virtual key processing.
#define DEBUG_VIRTUAL_KEYS 0
// Log debug messages about pointers.
#define DEBUG_POINTERS 0
// Log debug messages about pointer assignment calculations.
#define DEBUG_POINTER_ASSIGNMENT 0
#include <cutils/log.h>
#include <ui/InputDevice.h>
#include <stddef.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
/* Slop distance for jumpy pointer detection.
* The vertical range of the screen divided by this is our epsilon value. */
#define JUMPY_EPSILON_DIVISOR 212
/* Number of jumpy points to drop for touchscreens that need it. */
#define JUMPY_TRANSITION_DROPS 3
#define JUMPY_DROP_LIMIT 3
/* Maximum squared distance for averaging.
* If moving farther than this, turn of averaging to avoid lag in response. */
#define AVERAGING_DISTANCE_LIMIT (75 * 75)
namespace android {
// --- Static Functions ---
template<typename T>
inline static T abs(const T& value) {
return value < 0 ? - value : value;
}
template<typename T>
inline static T min(const T& a, const T& b) {
return a < b ? a : b;
}
template<typename T>
inline static void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// --- InputDevice ---
InputDevice::InputDevice(int32_t id, uint32_t classes, String8 name) :
id(id), classes(classes), name(name), ignored(false) {
}
void InputDevice::reset() {
if (isKeyboard()) {
keyboard.reset();
}
if (isTrackball()) {
trackball.reset();
}
if (isMultiTouchScreen()) {
multiTouchScreen.reset();
} else if (isSingleTouchScreen()) {
singleTouchScreen.reset();
}
if (isTouchScreen()) {
touchScreen.reset();
}
}
// --- InputDevice::TouchData ---
void InputDevice::TouchData::copyFrom(const TouchData& other) {
pointerCount = other.pointerCount;
idBits = other.idBits;
for (uint32_t i = 0; i < pointerCount; i++) {
pointers[i] = other.pointers[i];
idToIndex[i] = other.idToIndex[i];
}
}
// --- InputDevice::KeyboardState ---
void InputDevice::KeyboardState::reset() {
current.metaState = META_NONE;
current.downTime = 0;
}
// --- InputDevice::TrackballState ---
void InputDevice::TrackballState::reset() {
accumulator.clear();
current.down = false;
current.downTime = 0;
}
// --- InputDevice::TouchScreenState ---
void InputDevice::TouchScreenState::reset() {
lastTouch.clear();
downTime = 0;
currentVirtualKey.status = CurrentVirtualKeyState::STATUS_UP;
for (uint32_t i = 0; i < MAX_POINTERS; i++) {
averagingTouchFilter.historyStart[i] = 0;
averagingTouchFilter.historyEnd[i] = 0;
}
jumpyTouchFilter.jumpyPointsDropped = 0;
}
struct PointerDistanceHeapElement {
uint32_t currentPointerIndex : 8;
uint32_t lastPointerIndex : 8;
uint64_t distance : 48; // squared distance
};
void InputDevice::TouchScreenState::calculatePointerIds() {
uint32_t currentPointerCount = currentTouch.pointerCount;
uint32_t lastPointerCount = lastTouch.pointerCount;
if (currentPointerCount == 0) {
// No pointers to assign.
currentTouch.idBits.clear();
} else if (lastPointerCount == 0) {
// All pointers are new.
currentTouch.idBits.clear();
for (uint32_t i = 0; i < currentPointerCount; i++) {
currentTouch.pointers[i].id = i;
currentTouch.idToIndex[i] = i;
currentTouch.idBits.markBit(i);
}
} else if (currentPointerCount == 1 && lastPointerCount == 1) {
// Only one pointer and no change in count so it must have the same id as before.
uint32_t id = lastTouch.pointers[0].id;
currentTouch.pointers[0].id = id;
currentTouch.idToIndex[id] = 0;
currentTouch.idBits.value = BitSet32::valueForBit(id);
} else {
// General case.
// We build a heap of squared euclidean distances between current and last pointers
// associated with the current and last pointer indices. Then, we find the best
// match (by distance) for each current pointer.
PointerDistanceHeapElement heap[MAX_POINTERS * MAX_POINTERS];
uint32_t heapSize = 0;
for (uint32_t currentPointerIndex = 0; currentPointerIndex < currentPointerCount;
currentPointerIndex++) {
for (uint32_t lastPointerIndex = 0; lastPointerIndex < lastPointerCount;
lastPointerIndex++) {
int64_t deltaX = currentTouch.pointers[currentPointerIndex].x
- lastTouch.pointers[lastPointerIndex].x;
int64_t deltaY = currentTouch.pointers[currentPointerIndex].y
- lastTouch.pointers[lastPointerIndex].y;
uint64_t distance = uint64_t(deltaX * deltaX + deltaY * deltaY);
// Insert new element into the heap (sift up).
heap[heapSize].currentPointerIndex = currentPointerIndex;
heap[heapSize].lastPointerIndex = lastPointerIndex;
heap[heapSize].distance = distance;
heapSize += 1;
}
}
// Heapify
for (uint32_t startIndex = heapSize / 2; startIndex != 0; ) {
startIndex -= 1;
for (uint32_t parentIndex = startIndex; ;) {
uint32_t childIndex = parentIndex * 2 + 1;
if (childIndex >= heapSize) {
break;
}
if (childIndex + 1 < heapSize
&& heap[childIndex + 1].distance < heap[childIndex].distance) {
childIndex += 1;
}
if (heap[parentIndex].distance <= heap[childIndex].distance) {
break;
}
swap(heap[parentIndex], heap[childIndex]);
parentIndex = childIndex;
}
}
#if DEBUG_POINTER_ASSIGNMENT
LOGD("calculatePointerIds - initial distance min-heap: size=%d", heapSize);
for (size_t i = 0; i < heapSize; i++) {
LOGD(" heap[%d]: cur=%d, last=%d, distance=%lld",
i, heap[i].currentPointerIndex, heap[i].lastPointerIndex,
heap[i].distance);
}
#endif
// Pull matches out by increasing order of distance.
// To avoid reassigning pointers that have already been matched, the loop keeps track
// of which last and current pointers have been matched using the matchedXXXBits variables.
// It also tracks the used pointer id bits.
BitSet32 matchedLastBits(0);
BitSet32 matchedCurrentBits(0);
BitSet32 usedIdBits(0);
bool first = true;
for (uint32_t i = min(currentPointerCount, lastPointerCount); i > 0; i--) {
for (;;) {
if (first) {
// The first time through the loop, we just consume the root element of
// the heap (the one with smallest distance).
first = false;
} else {
// Previous iterations consumed the root element of the heap.
// Pop root element off of the heap (sift down).
heapSize -= 1;
assert(heapSize > 0);
// Sift down.
heap[0] = heap[heapSize];
for (uint32_t parentIndex = 0; ;) {
uint32_t childIndex = parentIndex * 2 + 1;
if (childIndex >= heapSize) {
break;
}
if (childIndex + 1 < heapSize
&& heap[childIndex + 1].distance < heap[childIndex].distance) {
childIndex += 1;
}
if (heap[parentIndex].distance <= heap[childIndex].distance) {
break;
}
swap(heap[parentIndex], heap[childIndex]);
parentIndex = childIndex;
}
#if DEBUG_POINTER_ASSIGNMENT
LOGD("calculatePointerIds - reduced distance min-heap: size=%d", heapSize);
for (size_t i = 0; i < heapSize; i++) {
LOGD(" heap[%d]: cur=%d, last=%d, distance=%lld",
i, heap[i].currentPointerIndex, heap[i].lastPointerIndex,
heap[i].distance);
}
#endif
}
uint32_t currentPointerIndex = heap[0].currentPointerIndex;
if (matchedCurrentBits.hasBit(currentPointerIndex)) continue; // already matched
uint32_t lastPointerIndex = heap[0].lastPointerIndex;
if (matchedLastBits.hasBit(lastPointerIndex)) continue; // already matched
matchedCurrentBits.markBit(currentPointerIndex);
matchedLastBits.markBit(lastPointerIndex);
uint32_t id = lastTouch.pointers[lastPointerIndex].id;
currentTouch.pointers[currentPointerIndex].id = id;
currentTouch.idToIndex[id] = currentPointerIndex;
usedIdBits.markBit(id);
#if DEBUG_POINTER_ASSIGNMENT
LOGD("calculatePointerIds - matched: cur=%d, last=%d, id=%d, distance=%lld",
lastPointerIndex, currentPointerIndex, id, heap[0].distance);
#endif
break;
}
}
// Assign fresh ids to new pointers.
if (currentPointerCount > lastPointerCount) {
for (uint32_t i = currentPointerCount - lastPointerCount; ;) {
uint32_t currentPointerIndex = matchedCurrentBits.firstUnmarkedBit();
uint32_t id = usedIdBits.firstUnmarkedBit();
currentTouch.pointers[currentPointerIndex].id = id;
currentTouch.idToIndex[id] = currentPointerIndex;
usedIdBits.markBit(id);
#if DEBUG_POINTER_ASSIGNMENT
LOGD("calculatePointerIds - assigned: cur=%d, id=%d",
currentPointerIndex, id);
#endif
if (--i == 0) break; // done
matchedCurrentBits.markBit(currentPointerIndex);
}
}
// Fix id bits.
currentTouch.idBits = usedIdBits;
}
}
/* Special hack for devices that have bad screen data: if one of the
* points has moved more than a screen height from the last position,
* then drop it. */
bool InputDevice::TouchScreenState::applyBadTouchFilter() {
// This hack requires valid axis parameters.
if (! parameters.yAxis.valid) {
return false;
}
uint32_t pointerCount = currentTouch.pointerCount;
// Nothing to do if there are no points.
if (pointerCount == 0) {
return false;
}
// Don't do anything if a finger is going down or up. We run
// here before assigning pointer IDs, so there isn't a good
// way to do per-finger matching.
if (pointerCount != lastTouch.pointerCount) {
return false;
}
// We consider a single movement across more than a 7/16 of
// the long size of the screen to be bad. This was a magic value
// determined by looking at the maximum distance it is feasible
// to actually move in one sample.
int32_t maxDeltaY = parameters.yAxis.range * 7 / 16;
// XXX The original code in InputDevice.java included commented out
// code for testing the X axis. Note that when we drop a point
// we don't actually restore the old X either. Strange.
// The old code also tries to track when bad points were previously
// detected but it turns out that due to the placement of a "break"
// at the end of the loop, we never set mDroppedBadPoint to true
// so it is effectively dead code.
// Need to figure out if the old code is busted or just overcomplicated
// but working as intended.
// Look through all new points and see if any are farther than
// acceptable from all previous points.
for (uint32_t i = pointerCount; i-- > 0; ) {
int32_t y = currentTouch.pointers[i].y;
int32_t closestY = INT_MAX;
int32_t closestDeltaY = 0;
#if DEBUG_HACKS
LOGD("BadTouchFilter: Looking at next point #%d: y=%d", i, y);
#endif
for (uint32_t j = pointerCount; j-- > 0; ) {
int32_t lastY = lastTouch.pointers[j].y;
int32_t deltaY = abs(y - lastY);
#if DEBUG_HACKS
LOGD("BadTouchFilter: Comparing with last point #%d: y=%d deltaY=%d",
j, lastY, deltaY);
#endif
if (deltaY < maxDeltaY) {
goto SkipSufficientlyClosePoint;
}
if (deltaY < closestDeltaY) {
closestDeltaY = deltaY;
closestY = lastY;
}
}
// Must not have found a close enough match.
#if DEBUG_HACKS
LOGD("BadTouchFilter: Dropping bad point #%d: newY=%d oldY=%d deltaY=%d maxDeltaY=%d",
i, y, closestY, closestDeltaY, maxDeltaY);
#endif
currentTouch.pointers[i].y = closestY;
return true; // XXX original code only corrects one point
SkipSufficientlyClosePoint: ;
}
// No change.
return false;
}
/* Special hack for devices that have bad screen data: drop points where
* the coordinate value for one axis has jumped to the other pointer's location.
*/
bool InputDevice::TouchScreenState::applyJumpyTouchFilter() {
// This hack requires valid axis parameters.
if (! parameters.yAxis.valid) {
return false;
}
uint32_t pointerCount = currentTouch.pointerCount;
if (lastTouch.pointerCount != pointerCount) {
#if DEBUG_HACKS
LOGD("JumpyTouchFilter: Different pointer count %d -> %d",
lastTouch.pointerCount, pointerCount);
for (uint32_t i = 0; i < pointerCount; i++) {
LOGD(" Pointer %d (%d, %d)", i,
currentTouch.pointers[i].x, currentTouch.pointers[i].y);
}
#endif
if (jumpyTouchFilter.jumpyPointsDropped < JUMPY_TRANSITION_DROPS) {
if (lastTouch.pointerCount == 1 && pointerCount == 2) {
// Just drop the first few events going from 1 to 2 pointers.
// They're bad often enough that they're not worth considering.
currentTouch.pointerCount = 1;
jumpyTouchFilter.jumpyPointsDropped += 1;
#if DEBUG_HACKS
LOGD("JumpyTouchFilter: Pointer 2 dropped");
#endif
return true;
} else if (lastTouch.pointerCount == 2 && pointerCount == 1) {
// The event when we go from 2 -> 1 tends to be messed up too
currentTouch.pointerCount = 2;
currentTouch.pointers[0] = lastTouch.pointers[0];
currentTouch.pointers[1] = lastTouch.pointers[1];
jumpyTouchFilter.jumpyPointsDropped += 1;
#if DEBUG_HACKS
for (int32_t i = 0; i < 2; i++) {
LOGD("JumpyTouchFilter: Pointer %d replaced (%d, %d)", i,
currentTouch.pointers[i].x, currentTouch.pointers[i].y);
}
#endif
return true;
}
}
// Reset jumpy points dropped on other transitions or if limit exceeded.
jumpyTouchFilter.jumpyPointsDropped = 0;
#if DEBUG_HACKS
LOGD("JumpyTouchFilter: Transition - drop limit reset");
#endif
return false;
}
// We have the same number of pointers as last time.
// A 'jumpy' point is one where the coordinate value for one axis
// has jumped to the other pointer's location. No need to do anything
// else if we only have one pointer.
if (pointerCount < 2) {
return false;
}
if (jumpyTouchFilter.jumpyPointsDropped < JUMPY_DROP_LIMIT) {
int jumpyEpsilon = parameters.yAxis.range / JUMPY_EPSILON_DIVISOR;
// We only replace the single worst jumpy point as characterized by pointer distance
// in a single axis.
int32_t badPointerIndex = -1;
int32_t badPointerReplacementIndex = -1;
int32_t badPointerDistance = INT_MIN; // distance to be corrected
for (uint32_t i = pointerCount; i-- > 0; ) {
int32_t x = currentTouch.pointers[i].x;
int32_t y = currentTouch.pointers[i].y;
#if DEBUG_HACKS
LOGD("JumpyTouchFilter: Point %d (%d, %d)", i, x, y);
#endif
// Check if a touch point is too close to another's coordinates
bool dropX = false, dropY = false;
for (uint32_t j = 0; j < pointerCount; j++) {
if (i == j) {
continue;
}
if (abs(x - currentTouch.pointers[j].x) <= jumpyEpsilon) {
dropX = true;
break;
}
if (abs(y - currentTouch.pointers[j].y) <= jumpyEpsilon) {
dropY = true;
break;
}
}
if (! dropX && ! dropY) {
continue; // not jumpy
}
// Find a replacement candidate by comparing with older points on the
// complementary (non-jumpy) axis.
int32_t distance = INT_MIN; // distance to be corrected
int32_t replacementIndex = -1;
if (dropX) {
// X looks too close. Find an older replacement point with a close Y.
int32_t smallestDeltaY = INT_MAX;
for (uint32_t j = 0; j < pointerCount; j++) {
int32_t deltaY = abs(y - lastTouch.pointers[j].y);
if (deltaY < smallestDeltaY) {
smallestDeltaY = deltaY;
replacementIndex = j;
}
}
distance = abs(x - lastTouch.pointers[replacementIndex].x);
} else {
// Y looks too close. Find an older replacement point with a close X.
int32_t smallestDeltaX = INT_MAX;
for (uint32_t j = 0; j < pointerCount; j++) {
int32_t deltaX = abs(x - lastTouch.pointers[j].x);
if (deltaX < smallestDeltaX) {
smallestDeltaX = deltaX;
replacementIndex = j;
}
}
distance = abs(y - lastTouch.pointers[replacementIndex].y);
}
// If replacing this pointer would correct a worse error than the previous ones
// considered, then use this replacement instead.
if (distance > badPointerDistance) {
badPointerIndex = i;
badPointerReplacementIndex = replacementIndex;
badPointerDistance = distance;
}
}
// Correct the jumpy pointer if one was found.
if (badPointerIndex >= 0) {
#if DEBUG_HACKS
LOGD("JumpyTouchFilter: Replacing bad pointer %d with (%d, %d)",
badPointerIndex,
lastTouch.pointers[badPointerReplacementIndex].x,
lastTouch.pointers[badPointerReplacementIndex].y);
#endif
currentTouch.pointers[badPointerIndex].x =
lastTouch.pointers[badPointerReplacementIndex].x;
currentTouch.pointers[badPointerIndex].y =
lastTouch.pointers[badPointerReplacementIndex].y;
jumpyTouchFilter.jumpyPointsDropped += 1;
return true;
}
}
jumpyTouchFilter.jumpyPointsDropped = 0;
return false;
}
/* Special hack for devices that have bad screen data: aggregate and
* compute averages of the coordinate data, to reduce the amount of
* jitter seen by applications. */
void InputDevice::TouchScreenState::applyAveragingTouchFilter() {
for (uint32_t currentIndex = 0; currentIndex < currentTouch.pointerCount; currentIndex++) {
uint32_t id = currentTouch.pointers[currentIndex].id;
int32_t x = currentTouch.pointers[currentIndex].x;
int32_t y = currentTouch.pointers[currentIndex].y;
int32_t pressure = currentTouch.pointers[currentIndex].pressure;
if (lastTouch.idBits.hasBit(id)) {
// Pointer was down before and is still down now.
// Compute average over history trace.
uint32_t start = averagingTouchFilter.historyStart[id];
uint32_t end = averagingTouchFilter.historyEnd[id];
int64_t deltaX = x - averagingTouchFilter.historyData[end].pointers[id].x;
int64_t deltaY = y - averagingTouchFilter.historyData[end].pointers[id].y;
uint64_t distance = uint64_t(deltaX * deltaX + deltaY * deltaY);
#if DEBUG_HACKS
LOGD("AveragingTouchFilter: Pointer id %d - Distance from last sample: %lld",
id, distance);
#endif
if (distance < AVERAGING_DISTANCE_LIMIT) {
// Increment end index in preparation for recording new historical data.
end += 1;
if (end > AVERAGING_HISTORY_SIZE) {
end = 0;
}
// If the end index has looped back to the start index then we have filled
// the historical trace up to the desired size so we drop the historical
// data at the start of the trace.
if (end == start) {
start += 1;
if (start > AVERAGING_HISTORY_SIZE) {
start = 0;
}
}
// Add the raw data to the historical trace.
averagingTouchFilter.historyStart[id] = start;
averagingTouchFilter.historyEnd[id] = end;
averagingTouchFilter.historyData[end].pointers[id].x = x;
averagingTouchFilter.historyData[end].pointers[id].y = y;
averagingTouchFilter.historyData[end].pointers[id].pressure = pressure;
// Average over all historical positions in the trace by total pressure.
int32_t averagedX = 0;
int32_t averagedY = 0;
int32_t totalPressure = 0;
for (;;) {
int32_t historicalX = averagingTouchFilter.historyData[start].pointers[id].x;
int32_t historicalY = averagingTouchFilter.historyData[start].pointers[id].y;
int32_t historicalPressure = averagingTouchFilter.historyData[start]
.pointers[id].pressure;
averagedX += historicalX * historicalPressure;
averagedY += historicalY * historicalPressure;
totalPressure += historicalPressure;
if (start == end) {
break;
}
start += 1;
if (start > AVERAGING_HISTORY_SIZE) {
start = 0;
}
}
averagedX /= totalPressure;
averagedY /= totalPressure;
#if DEBUG_HACKS
LOGD("AveragingTouchFilter: Pointer id %d - "
"totalPressure=%d, averagedX=%d, averagedY=%d", id, totalPressure,
averagedX, averagedY);
#endif
currentTouch.pointers[currentIndex].x = averagedX;
currentTouch.pointers[currentIndex].y = averagedY;
} else {
#if DEBUG_HACKS
LOGD("AveragingTouchFilter: Pointer id %d - Exceeded max distance", id);
#endif
}
} else {
#if DEBUG_HACKS
LOGD("AveragingTouchFilter: Pointer id %d - Pointer went up", id);
#endif
}
// Reset pointer history.
averagingTouchFilter.historyStart[id] = 0;
averagingTouchFilter.historyEnd[id] = 0;
averagingTouchFilter.historyData[0].pointers[id].x = x;
averagingTouchFilter.historyData[0].pointers[id].y = y;
averagingTouchFilter.historyData[0].pointers[id].pressure = pressure;
}
}
bool InputDevice::TouchScreenState::isPointInsideDisplay(int32_t x, int32_t y) const {
if (! parameters.xAxis.valid || ! parameters.yAxis.valid) {
// Assume all points on a touch screen without valid axis parameters are
// inside the display.
return true;
}
return x >= parameters.xAxis.minValue
&& x <= parameters.xAxis.maxValue
&& y >= parameters.yAxis.minValue
&& y <= parameters.yAxis.maxValue;
}
const InputDevice::VirtualKey* InputDevice::TouchScreenState::findVirtualKeyHit() const {
int32_t x = currentTouch.pointers[0].x;
int32_t y = currentTouch.pointers[0].y;
for (size_t i = 0; i < virtualKeys.size(); i++) {
const InputDevice::VirtualKey& virtualKey = virtualKeys[i];
#if DEBUG_VIRTUAL_KEYS
LOGD("VirtualKeys: Hit test (%d, %d): keyCode=%d, scanCode=%d, "
"left=%d, top=%d, right=%d, bottom=%d",
x, y,
virtualKey.keyCode, virtualKey.scanCode,
virtualKey.hitLeft, virtualKey.hitTop,
virtualKey.hitRight, virtualKey.hitBottom);
#endif
if (virtualKey.isHit(x, y)) {
return & virtualKey;
}
}
return NULL;
}
// --- InputDevice::SingleTouchScreenState ---
void InputDevice::SingleTouchScreenState::reset() {
accumulator.clear();
current.down = false;
current.x = 0;
current.y = 0;
current.pressure = 0;
current.size = 0;
}
// --- InputDevice::MultiTouchScreenState ---
void InputDevice::MultiTouchScreenState::reset() {
accumulator.clear();
}
} // namespace android