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