1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
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
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package com.android.dumprendertree2;
18
19import android.os.Bundle;
20import android.os.Handler;
21import android.os.Message;
22import android.os.SystemClock;
23import android.util.Log;
24import android.view.KeyEvent;
25import android.view.MotionEvent;
26import android.webkit.WebView;
27
28import java.util.LinkedList;
29import java.util.List;
30
31/**
32 * An implementation of EventSender
33 */
34public class EventSenderImpl {
35    private static final String LOG_TAG = "EventSenderImpl";
36
37    private static final int MSG_ENABLE_DOM_UI_EVENT_LOGGING = 0;
38    private static final int MSG_FIRE_KEYBOARD_EVENTS_TO_ELEMENT = 1;
39    private static final int MSG_LEAP_FORWARD = 2;
40
41    private static final int MSG_KEY_DOWN = 3;
42
43    private static final int MSG_MOUSE_DOWN = 4;
44    private static final int MSG_MOUSE_UP = 5;
45    private static final int MSG_MOUSE_CLICK = 6;
46    private static final int MSG_MOUSE_MOVE_TO = 7;
47
48    private static final int MSG_ADD_TOUCH_POINT = 8;
49    private static final int MSG_TOUCH_START = 9;
50    private static final int MSG_UPDATE_TOUCH_POINT = 10;
51    private static final int MSG_TOUCH_MOVE = 11;
52    private static final int MSG_CLEAR_TOUCH_POINTS = 12;
53    private static final int MSG_TOUCH_CANCEL = 13;
54    private static final int MSG_RELEASE_TOUCH_POINT = 14;
55    private static final int MSG_TOUCH_END = 15;
56    private static final int MSG_SET_TOUCH_MODIFIER = 16;
57    private static final int MSG_CANCEL_TOUCH_POINT = 17;
58
59    private static class Point {
60        private int mX;
61        private int mY;
62
63        public Point(int x, int y) {
64            mX = x;
65            mY = y;
66        }
67        public int x() {
68            return mX;
69        }
70        public int y() {
71            return mY;
72        }
73    }
74
75    private Point createViewPointFromContentCoordinates(int x, int y) {
76        return new Point(Math.round(x * mWebView.getScale()) - mWebView.getScrollX(),
77                         Math.round(y * mWebView.getScale()) - mWebView.getScrollY());
78    }
79
80    public static class TouchPoint {
81        private int mId;
82        private Point mPoint;
83        private long mDownTime;
84        private boolean mReleased = false;
85        private boolean mMoved = false;
86        private boolean mCancelled = false;
87
88        public TouchPoint(int id, Point point) {
89            mId = id;
90            mPoint = point;
91        }
92
93        public int getId() {
94          return mId;
95        }
96
97        public int getX() {
98            return mPoint.x();
99        }
100
101        public int getY() {
102            return mPoint.y();
103        }
104
105        public boolean hasMoved() {
106            return mMoved;
107        }
108
109        public void move(Point point) {
110            mPoint = point;
111            mMoved = true;
112        }
113
114        public void resetHasMoved() {
115            mMoved = false;
116        }
117
118        public long getDownTime() {
119            return mDownTime;
120        }
121
122        public void setDownTime(long downTime) {
123            mDownTime = downTime;
124        }
125
126        public boolean isReleased() {
127            return mReleased;
128        }
129
130        public void release() {
131            mReleased = true;
132        }
133
134        public boolean isCancelled() {
135            return mCancelled;
136        }
137
138        public void cancel() {
139            mCancelled = true;
140        }
141    }
142
143    private List<TouchPoint> mTouchPoints;
144    private int mTouchMetaState;
145    private Point mMousePoint;
146
147    private WebView mWebView;
148
149    private Handler mEventSenderHandler = new Handler() {
150        @Override
151        public void handleMessage(Message msg) {
152            Bundle bundle;
153            MotionEvent event;
154            long ts;
155
156            switch (msg.what) {
157                case MSG_ENABLE_DOM_UI_EVENT_LOGGING:
158                    /** TODO: implement */
159                    break;
160
161                case MSG_FIRE_KEYBOARD_EVENTS_TO_ELEMENT:
162                    /** TODO: implement */
163                    break;
164
165                case MSG_LEAP_FORWARD:
166                    /** TODO: implement */
167                    break;
168
169                case MSG_KEY_DOWN:
170                    bundle = (Bundle)msg.obj;
171                    String character = bundle.getString("character");
172                    String[] withModifiers = bundle.getStringArray("withModifiers");
173
174                    if (withModifiers != null && withModifiers.length > 0) {
175                        for (int i = 0; i < withModifiers.length; i++) {
176                            executeKeyEvent(KeyEvent.ACTION_DOWN,
177                                    modifierToKeyCode(withModifiers[i]));
178                        }
179                    }
180                    executeKeyEvent(KeyEvent.ACTION_DOWN,
181                            charToKeyCode(character.toLowerCase().toCharArray()[0]));
182                    break;
183
184                /** MOUSE */
185
186                case MSG_MOUSE_DOWN:
187                    if (mMousePoint != null) {
188                        ts = SystemClock.uptimeMillis();
189                        event = MotionEvent.obtain(ts, ts, MotionEvent.ACTION_DOWN, mMousePoint.x(), mMousePoint.y(), 0);
190                        mWebView.onTouchEvent(event);
191                    }
192                    break;
193
194                case MSG_MOUSE_UP:
195                    if (mMousePoint != null) {
196                        ts = SystemClock.uptimeMillis();
197                        event = MotionEvent.obtain(ts, ts, MotionEvent.ACTION_UP, mMousePoint.x(), mMousePoint.y(), 0);
198                        mWebView.onTouchEvent(event);
199                    }
200                    break;
201
202                case MSG_MOUSE_CLICK:
203                    mouseDown();
204                    mouseUp();
205                    break;
206
207                case MSG_MOUSE_MOVE_TO:
208                    mMousePoint = createViewPointFromContentCoordinates(msg.arg1, msg.arg2);
209                    break;
210
211                /** TOUCH */
212
213                case MSG_ADD_TOUCH_POINT:
214                    int numPoints = getTouchPoints().size();
215                    int id;
216                    if (numPoints == 0) {
217                        id = 0;
218                    } else {
219                        id = getTouchPoints().get(numPoints - 1).getId() + 1;
220                    }
221                    getTouchPoints().add(
222                            new TouchPoint(id, createViewPointFromContentCoordinates(msg.arg1, msg.arg2)));
223                    break;
224
225                case MSG_TOUCH_START:
226                    if (getTouchPoints().isEmpty()) {
227                        return;
228                    }
229                    for (int i = 0; i < getTouchPoints().size(); ++i) {
230                        getTouchPoints().get(i).setDownTime(SystemClock.uptimeMillis());
231                    }
232                    executeTouchEvent(MotionEvent.ACTION_DOWN);
233                    break;
234
235                case MSG_UPDATE_TOUCH_POINT:
236                    bundle = (Bundle)msg.obj;
237
238                    int index = bundle.getInt("id");
239                    if (index >= getTouchPoints().size()) {
240                        Log.w(LOG_TAG + "::MSG_UPDATE_TOUCH_POINT", "TouchPoint out of bounds: "
241                                + index);
242                        break;
243                    }
244
245                    getTouchPoints().get(index).move(
246                            createViewPointFromContentCoordinates(bundle.getInt("x"), bundle.getInt("y")));
247                    break;
248
249                case MSG_TOUCH_MOVE:
250                    /**
251                     * FIXME: At the moment we don't support multi-touch. Hence, we only examine
252                     * the first touch point. In future this method will need rewriting.
253                     */
254                    if (getTouchPoints().isEmpty()) {
255                        return;
256                    }
257                    executeTouchEvent(MotionEvent.ACTION_MOVE);
258                    for (int i = 0; i < getTouchPoints().size(); ++i) {
259                        getTouchPoints().get(i).resetHasMoved();
260                    }
261                    break;
262
263                case MSG_CANCEL_TOUCH_POINT:
264                    if (msg.arg1 >= getTouchPoints().size()) {
265                        Log.w(LOG_TAG + "::MSG_RELEASE_TOUCH_POINT", "TouchPoint out of bounds: "
266                                + msg.arg1);
267                        break;
268                    }
269
270                    getTouchPoints().get(msg.arg1).cancel();
271                    break;
272
273                case MSG_TOUCH_CANCEL:
274                    /**
275                     * FIXME: At the moment we don't support multi-touch. Hence, we only examine
276                     * the first touch point. In future this method will need rewriting.
277                     */
278                    if (getTouchPoints().isEmpty()) {
279                        return;
280                    }
281                    executeTouchEvent(MotionEvent.ACTION_CANCEL);
282                    break;
283
284                case MSG_RELEASE_TOUCH_POINT:
285                    if (msg.arg1 >= getTouchPoints().size()) {
286                        Log.w(LOG_TAG + "::MSG_RELEASE_TOUCH_POINT", "TouchPoint out of bounds: "
287                                + msg.arg1);
288                        break;
289                    }
290
291                    getTouchPoints().get(msg.arg1).release();
292                    break;
293
294                case MSG_TOUCH_END:
295                    /**
296                     * FIXME: At the moment we don't support multi-touch. Hence, we only examine
297                     * the first touch point. In future this method will need rewriting.
298                     */
299                    if (getTouchPoints().isEmpty()) {
300                        return;
301                    }
302                    executeTouchEvent(MotionEvent.ACTION_UP);
303                    // remove released points.
304                    for (int i = getTouchPoints().size() - 1; i >= 0; --i) {
305                        if (getTouchPoints().get(i).isReleased()) {
306                            getTouchPoints().remove(i);
307                        }
308                    }
309                    break;
310
311                case MSG_SET_TOUCH_MODIFIER:
312                    bundle = (Bundle)msg.obj;
313                    String modifier = bundle.getString("modifier");
314                    boolean enabled = bundle.getBoolean("enabled");
315
316                    int mask = 0;
317                    if ("alt".equals(modifier.toLowerCase())) {
318                        mask = KeyEvent.META_ALT_ON;
319                    } else if ("shift".equals(modifier.toLowerCase())) {
320                        mask = KeyEvent.META_SHIFT_ON;
321                    } else if ("ctrl".equals(modifier.toLowerCase())) {
322                        mask = KeyEvent.META_SYM_ON;
323                    }
324
325                    if (enabled) {
326                        mTouchMetaState |= mask;
327                    } else {
328                        mTouchMetaState &= ~mask;
329                    }
330
331                    break;
332
333                case MSG_CLEAR_TOUCH_POINTS:
334                    getTouchPoints().clear();
335                    break;
336
337                default:
338                    break;
339            }
340        }
341    };
342
343    public void reset(WebView webView) {
344        mWebView = webView;
345        mTouchPoints = null;
346        mTouchMetaState = 0;
347        mMousePoint = null;
348    }
349
350    public void enableDOMUIEventLogging(int domNode) {
351        Message msg = mEventSenderHandler.obtainMessage(MSG_ENABLE_DOM_UI_EVENT_LOGGING);
352        msg.arg1 = domNode;
353        msg.sendToTarget();
354    }
355
356    public void fireKeyboardEventsToElement(int domNode) {
357        Message msg = mEventSenderHandler.obtainMessage(MSG_FIRE_KEYBOARD_EVENTS_TO_ELEMENT);
358        msg.arg1 = domNode;
359        msg.sendToTarget();
360    }
361
362    public void leapForward(int milliseconds) {
363        Message msg = mEventSenderHandler.obtainMessage(MSG_LEAP_FORWARD);
364        msg.arg1 = milliseconds;
365        msg.sendToTarget();
366    }
367
368    public void keyDown(String character, String[] withModifiers) {
369        Bundle bundle = new Bundle();
370        bundle.putString("character", character);
371        bundle.putStringArray("withModifiers", withModifiers);
372        mEventSenderHandler.obtainMessage(MSG_KEY_DOWN, bundle).sendToTarget();
373    }
374
375    /** MOUSE */
376
377    public void mouseDown() {
378        mEventSenderHandler.sendEmptyMessage(MSG_MOUSE_DOWN);
379    }
380
381    public void mouseUp() {
382        mEventSenderHandler.sendEmptyMessage(MSG_MOUSE_UP);
383    }
384
385    public void mouseClick() {
386        mEventSenderHandler.sendEmptyMessage(MSG_MOUSE_CLICK);
387    }
388
389    public void mouseMoveTo(int x, int y) {
390        mEventSenderHandler.obtainMessage(MSG_MOUSE_MOVE_TO, x, y).sendToTarget();
391    }
392
393    /** TOUCH */
394
395    public void addTouchPoint(int x, int y) {
396        mEventSenderHandler.obtainMessage(MSG_ADD_TOUCH_POINT, x, y).sendToTarget();
397    }
398
399    public void touchStart() {
400        mEventSenderHandler.sendEmptyMessage(MSG_TOUCH_START);
401    }
402
403    public void updateTouchPoint(int id, int x, int y) {
404        Bundle bundle = new Bundle();
405        bundle.putInt("id", id);
406        bundle.putInt("x", x);
407        bundle.putInt("y", y);
408        mEventSenderHandler.obtainMessage(MSG_UPDATE_TOUCH_POINT, bundle).sendToTarget();
409    }
410
411    public void touchMove() {
412        mEventSenderHandler.sendEmptyMessage(MSG_TOUCH_MOVE);
413    }
414
415    public void cancelTouchPoint(int id) {
416        Message msg = mEventSenderHandler.obtainMessage(MSG_CANCEL_TOUCH_POINT);
417        msg.arg1 = id;
418        msg.sendToTarget();
419    }
420
421    public void touchCancel() {
422        mEventSenderHandler.sendEmptyMessage(MSG_TOUCH_CANCEL);
423    }
424
425    public void releaseTouchPoint(int id) {
426        Message msg = mEventSenderHandler.obtainMessage(MSG_RELEASE_TOUCH_POINT);
427        msg.arg1 = id;
428        msg.sendToTarget();
429    }
430
431    public void touchEnd() {
432        mEventSenderHandler.sendEmptyMessage(MSG_TOUCH_END);
433    }
434
435    public void setTouchModifier(String modifier, boolean enabled) {
436        Bundle bundle = new Bundle();
437        bundle.putString("modifier", modifier);
438        bundle.putBoolean("enabled", enabled);
439        mEventSenderHandler.obtainMessage(MSG_SET_TOUCH_MODIFIER, bundle).sendToTarget();
440    }
441
442    public void clearTouchPoints() {
443        mEventSenderHandler.sendEmptyMessage(MSG_CLEAR_TOUCH_POINTS);
444    }
445
446    private List<TouchPoint> getTouchPoints() {
447        if (mTouchPoints == null) {
448            mTouchPoints = new LinkedList<TouchPoint>();
449        }
450
451        return mTouchPoints;
452    }
453
454    private void executeTouchEvent(int action) {
455        int numPoints = getTouchPoints().size();
456        int[] pointerIds = new int[numPoints];
457        MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[numPoints];
458
459        for (int i = 0; i < numPoints; ++i) {
460            boolean isNeeded = false;
461            switch(action) {
462            case MotionEvent.ACTION_DOWN:
463            case MotionEvent.ACTION_UP:
464                isNeeded = true;
465                break;
466            case MotionEvent.ACTION_MOVE:
467                isNeeded = getTouchPoints().get(i).hasMoved();
468                break;
469            case MotionEvent.ACTION_CANCEL:
470                isNeeded = getTouchPoints().get(i).isCancelled();
471                break;
472            default:
473                Log.w(LOG_TAG + "::executeTouchEvent(),", "action not supported:" + action);
474                break;
475            }
476
477            numPoints = 0;
478            if (isNeeded) {
479                pointerIds[numPoints] = getTouchPoints().get(i).getId();
480                pointerCoords[numPoints] = new MotionEvent.PointerCoords();
481                pointerCoords[numPoints].x = getTouchPoints().get(i).getX();
482                pointerCoords[numPoints].y = getTouchPoints().get(i).getY();
483                ++numPoints;
484            }
485        }
486
487        if (numPoints == 0) {
488            return;
489        }
490
491        MotionEvent event = MotionEvent.obtain(mTouchPoints.get(0).getDownTime(),
492                SystemClock.uptimeMillis(), action,
493                numPoints, pointerIds, pointerCoords,
494                mTouchMetaState, 1.0f, 1.0f, 0, 0, 0, 0);
495
496        mWebView.onTouchEvent(event);
497    }
498
499    private void executeKeyEvent(int action, int keyCode) {
500        KeyEvent event = new KeyEvent(action, keyCode);
501        mWebView.onKeyDown(event.getKeyCode(), event);
502    }
503
504    /**
505     * Assumes lowercase chars, case needs to be handled by calling function.
506     */
507    private static int charToKeyCode(char c) {
508        // handle numbers
509        if (c >= '0' && c <= '9') {
510            int offset = c - '0';
511            return KeyEvent.KEYCODE_0 + offset;
512        }
513
514        // handle characters
515        if (c >= 'a' && c <= 'z') {
516            int offset = c - 'a';
517            return KeyEvent.KEYCODE_A + offset;
518        }
519
520        // handle all others
521        switch (c) {
522            case '*':
523                return KeyEvent.KEYCODE_STAR;
524
525            case '#':
526                return KeyEvent.KEYCODE_POUND;
527
528            case ',':
529                return KeyEvent.KEYCODE_COMMA;
530
531            case '.':
532                return KeyEvent.KEYCODE_PERIOD;
533
534            case '\t':
535                return KeyEvent.KEYCODE_TAB;
536
537            case ' ':
538                return KeyEvent.KEYCODE_SPACE;
539
540            case '\n':
541                return KeyEvent.KEYCODE_ENTER;
542
543            case '\b':
544            case 0x7F:
545                return KeyEvent.KEYCODE_DEL;
546
547            case '~':
548                return KeyEvent.KEYCODE_GRAVE;
549
550            case '-':
551                return KeyEvent.KEYCODE_MINUS;
552
553            case '=':
554                return KeyEvent.KEYCODE_EQUALS;
555
556            case '(':
557                return KeyEvent.KEYCODE_LEFT_BRACKET;
558
559            case ')':
560                return KeyEvent.KEYCODE_RIGHT_BRACKET;
561
562            case '\\':
563                return KeyEvent.KEYCODE_BACKSLASH;
564
565            case ';':
566                return KeyEvent.KEYCODE_SEMICOLON;
567
568            case '\'':
569                return KeyEvent.KEYCODE_APOSTROPHE;
570
571            case '/':
572                return KeyEvent.KEYCODE_SLASH;
573
574            default:
575                return c;
576        }
577    }
578
579    private static int modifierToKeyCode(String modifier) {
580        if (modifier.equals("ctrlKey")) {
581            return KeyEvent.KEYCODE_ALT_LEFT;
582        } else if (modifier.equals("shiftKey")) {
583            return KeyEvent.KEYCODE_SHIFT_LEFT;
584        } else if (modifier.equals("altKey")) {
585            return KeyEvent.KEYCODE_SYM;
586        }
587
588        return KeyEvent.KEYCODE_UNKNOWN;
589    }
590}
591