1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
|
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.util.Log;
/**
* This class creates DPAD events from touchpad events.
*
* @see ViewRootImpl
*/
//TODO: Make this class an internal class of ViewRootImpl.java
class SimulatedDpad {
private static final String TAG = "SimulatedDpad";
// Maximum difference in milliseconds between the down and up of a touch
// event for it to be considered a tap
// TODO:Read this value from a configuration file
private static final int MAX_TAP_TIME = 250;
// Where the cutoff is for determining an edge swipe
private static final float EDGE_SWIPE_THRESHOLD = 0.9f;
private static final int MSG_FLICK = 313;
// TODO: Pass touch slop from the input device
private static final int TOUCH_SLOP = 30;
// The position of the previous touchpad event
private float mLastTouchpadXPosition;
private float mLastTouchpadYPosition;
// Where the touchpad was initially pressed
private float mTouchpadEnterXPosition;
private float mTouchpadEnterYPosition;
// When the most recent ACTION_HOVER_ENTER occurred
private long mLastTouchPadStartTimeMs = 0;
// When the most recent direction key was sent
private long mLastTouchPadKeySendTimeMs = 0;
// When the most recent touch event of any type occurred
private long mLastTouchPadEventTimeMs = 0;
// Did the swipe begin in a valid region
private boolean mEdgeSwipePossible;
private final Context mContext;
// How quickly keys were sent;
private int mKeySendRateMs = 0;
private int mLastKeySent;
// Last movement in device screen pixels
private float mLastMoveX = 0;
private float mLastMoveY = 0;
// Offset from the initial touch. Gets reset as direction keys are sent.
private float mAccumulatedX;
private float mAccumulatedY;
// Change in position allowed during tap events
private float mTouchSlop;
private float mTouchSlopSquared;
// Has the TouchSlop constraint been invalidated
private boolean mAlwaysInTapRegion = true;
// Information from the most recent event.
// Used to determine what device sent the event during a fling.
private int mLastSource;
private int mLastMetaState;
private int mLastDeviceId;
// TODO: Currently using screen dimensions tuned to a Galaxy Nexus, need to
// read this from a config file instead
private int mDistancePerTick;
private int mDistancePerTickSquared;
// Highest rate that the flinged events can occur at before dying out
private int mMaxRepeatDelay;
// The square of the minimum distance needed for a flick to register
private int mMinFlickDistanceSquared;
// How quickly the repeated events die off
private float mFlickDecay;
public SimulatedDpad(Context context) {
mDistancePerTick = SystemProperties.getInt("persist.vr_dist_tick", 64);
mDistancePerTickSquared = mDistancePerTick * mDistancePerTick;
mMaxRepeatDelay = SystemProperties.getInt("persist.vr_repeat_delay", 300);
mMinFlickDistanceSquared = SystemProperties.getInt("persist.vr_min_flick", 20);
mMinFlickDistanceSquared *= mMinFlickDistanceSquared;
mFlickDecay = Float.parseFloat(SystemProperties.get(
"persist.sys.vr_flick_decay", "1.3"));
mTouchSlop = TOUCH_SLOP;
mTouchSlopSquared = mTouchSlop * mTouchSlop;
mContext = context;
}
private final Handler mHandler = new Handler(true /*async*/) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_FLICK: {
final long time = SystemClock.uptimeMillis();
ViewRootImpl viewroot = (ViewRootImpl) msg.obj;
// Send the key
viewroot.enqueueInputEvent(new KeyEvent(time, time,
KeyEvent.ACTION_DOWN, msg.arg2, 0, mLastMetaState,
mLastDeviceId, 0, KeyEvent.FLAG_FALLBACK, mLastSource));
viewroot.enqueueInputEvent(new KeyEvent(time, time,
KeyEvent.ACTION_UP, msg.arg2, 0, mLastMetaState,
mLastDeviceId, 0, KeyEvent.FLAG_FALLBACK, mLastSource));
// Increase the delay by the decay factor and resend
final int delay = (int) Math.ceil(mFlickDecay * msg.arg1);
if (delay <= mMaxRepeatDelay) {
Message msgCopy = Message.obtain(msg);
msgCopy.arg1 = delay;
msgCopy.setAsynchronous(true);
mHandler.sendMessageDelayed(msgCopy, delay);
}
break;
}
}
}
};
public void updateTouchPad(ViewRootImpl viewroot, MotionEvent event,
boolean synthesizeNewKeys) {
if (!synthesizeNewKeys) {
mHandler.removeMessages(MSG_FLICK);
}
InputDevice device = event.getDevice();
if (device == null) {
return;
}
// Store what time the touchpad event occurred
final long time = SystemClock.uptimeMillis();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastTouchPadStartTimeMs = time;
mAlwaysInTapRegion = true;
mTouchpadEnterXPosition = event.getX();
mTouchpadEnterYPosition = event.getY();
mAccumulatedX = 0;
mAccumulatedY = 0;
mLastMoveX = 0;
mLastMoveY = 0;
if (device.getMotionRange(MotionEvent.AXIS_Y).getMax()
* EDGE_SWIPE_THRESHOLD < event.getY()) {
// Did the swipe begin in a valid region
mEdgeSwipePossible = true;
}
// Clear any flings
if (synthesizeNewKeys) {
mHandler.removeMessages(MSG_FLICK);
}
break;
case MotionEvent.ACTION_MOVE:
// Determine whether the move is slop or an intentional move
float deltaX = event.getX() - mTouchpadEnterXPosition;
float deltaY = event.getY() - mTouchpadEnterYPosition;
if (mTouchSlopSquared < deltaX * deltaX + deltaY * deltaY) {
mAlwaysInTapRegion = false;
}
// Checks if the swipe has crossed the midpoint
// and if our swipe gesture is complete
if (event.getY() < (device.getMotionRange(MotionEvent.AXIS_Y).getMax()
* .5) && mEdgeSwipePossible) {
mEdgeSwipePossible = false;
Intent intent =
((SearchManager)mContext.getSystemService(Context.SEARCH_SERVICE))
.getAssistIntent(mContext, UserHandle.USER_CURRENT_OR_SELF);
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
mContext.startActivity(intent);
} catch (ActivityNotFoundException e){
Log.e(TAG, "Could not start search activity");
}
} else {
Log.e(TAG, "Could not find a search activity");
}
}
// Find the difference in position between the two most recent
// touchpad events
mLastMoveX = event.getX() - mLastTouchpadXPosition;
mLastMoveY = event.getY() - mLastTouchpadYPosition;
mAccumulatedX += mLastMoveX;
mAccumulatedY += mLastMoveY;
float mAccumulatedXSquared = mAccumulatedX * mAccumulatedX;
float mAccumulatedYSquared = mAccumulatedY * mAccumulatedY;
// Determine if we've moved far enough to send a key press
if (mAccumulatedXSquared > mDistancePerTickSquared ||
mAccumulatedYSquared > mDistancePerTickSquared) {
float dominantAxis;
float sign;
boolean isXAxis;
int key;
int repeatCount = 0;
// Determine dominant axis
if (mAccumulatedXSquared > mAccumulatedYSquared) {
dominantAxis = mAccumulatedX;
isXAxis = true;
} else {
dominantAxis = mAccumulatedY;
isXAxis = false;
}
// Determine sign of axis
sign = (dominantAxis > 0) ? 1 : -1;
// Determine key to send
if (isXAxis) {
key = (sign == 1) ? KeyEvent.KEYCODE_DPAD_RIGHT :
KeyEvent.KEYCODE_DPAD_LEFT;
} else {
key = (sign == 1) ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP;
}
// Send key until maximum distance constraint is satisfied
while (dominantAxis * dominantAxis > mDistancePerTickSquared) {
repeatCount++;
dominantAxis -= sign * mDistancePerTick;
if (synthesizeNewKeys) {
viewroot.enqueueInputEvent(new KeyEvent(time, time,
KeyEvent.ACTION_DOWN, key, 0, event.getMetaState(),
event.getDeviceId(), 0, KeyEvent.FLAG_FALLBACK,
event.getSource()));
viewroot.enqueueInputEvent(new KeyEvent(time, time,
KeyEvent.ACTION_UP, key, 0, event.getMetaState(),
event.getDeviceId(), 0, KeyEvent.FLAG_FALLBACK,
event.getSource()));
}
}
// Save new axis values
mAccumulatedX = isXAxis ? dominantAxis : 0;
mAccumulatedY = isXAxis ? 0 : dominantAxis;
mLastKeySent = key;
mKeySendRateMs = (int) ((time - mLastTouchPadKeySendTimeMs) / repeatCount);
mLastTouchPadKeySendTimeMs = time;
}
break;
case MotionEvent.ACTION_UP:
if (time - mLastTouchPadStartTimeMs < MAX_TAP_TIME && mAlwaysInTapRegion) {
if (synthesizeNewKeys) {
viewroot.enqueueInputEvent(new KeyEvent(mLastTouchPadStartTimeMs, time,
KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0,
event.getMetaState(), event.getDeviceId(), 0,
KeyEvent.FLAG_FALLBACK, event.getSource()));
viewroot.enqueueInputEvent(new KeyEvent(mLastTouchPadStartTimeMs, time,
KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0,
event.getMetaState(), event.getDeviceId(), 0,
KeyEvent.FLAG_FALLBACK, event.getSource()));
}
} else {
float xMoveSquared = mLastMoveX * mLastMoveX;
float yMoveSquared = mLastMoveY * mLastMoveY;
// Determine whether the last gesture was a fling.
if (mMinFlickDistanceSquared <= xMoveSquared + yMoveSquared &&
time - mLastTouchPadEventTimeMs <= MAX_TAP_TIME &&
mKeySendRateMs <= mMaxRepeatDelay && mKeySendRateMs > 0) {
mLastDeviceId = event.getDeviceId();
mLastSource = event.getSource();
mLastMetaState = event.getMetaState();
if (synthesizeNewKeys) {
Message message = Message.obtain(mHandler, MSG_FLICK,
mKeySendRateMs, mLastKeySent, viewroot);
message.setAsynchronous(true);
mHandler.sendMessageDelayed(message, mKeySendRateMs);
}
}
}
mEdgeSwipePossible = false;
break;
}
// Store touch event position and time
mLastTouchPadEventTimeMs = time;
mLastTouchpadXPosition = event.getX();
mLastTouchpadYPosition = event.getY();
}
}
|