1/*
2 * Copyright (C) 2013 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.inputmethod.keyboard.emoji;
18
19import android.content.Context;
20import android.os.Handler;
21import android.util.AttributeSet;
22import android.view.GestureDetector;
23import android.view.MotionEvent;
24import android.view.accessibility.AccessibilityEvent;
25
26import com.android.inputmethod.accessibility.AccessibilityUtils;
27import com.android.inputmethod.accessibility.KeyboardAccessibilityDelegate;
28import com.android.inputmethod.keyboard.Key;
29import com.android.inputmethod.keyboard.KeyDetector;
30import com.android.inputmethod.keyboard.Keyboard;
31import com.android.inputmethod.keyboard.KeyboardView;
32import com.android.inputmethod.latin.R;
33
34/**
35 * This is an extended {@link KeyboardView} class that hosts an emoji page keyboard.
36 * Multi-touch unsupported. No gesture support.
37 */
38// TODO: Implement key popup preview.
39final class EmojiPageKeyboardView extends KeyboardView implements
40        GestureDetector.OnGestureListener {
41    private static final long KEY_PRESS_DELAY_TIME = 250;  // msec
42    private static final long KEY_RELEASE_DELAY_TIME = 30;  // msec
43
44    public interface OnKeyEventListener {
45        public void onPressKey(Key key);
46        public void onReleaseKey(Key key);
47    }
48
49    private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() {
50        @Override
51        public void onPressKey(final Key key) {}
52        @Override
53        public void onReleaseKey(final Key key) {}
54    };
55
56    private OnKeyEventListener mListener = EMPTY_LISTENER;
57    private final KeyDetector mKeyDetector = new KeyDetector();
58    private final GestureDetector mGestureDetector;
59    private KeyboardAccessibilityDelegate<EmojiPageKeyboardView> mAccessibilityDelegate;
60
61    public EmojiPageKeyboardView(final Context context, final AttributeSet attrs) {
62        this(context, attrs, R.attr.keyboardViewStyle);
63    }
64
65    public EmojiPageKeyboardView(final Context context, final AttributeSet attrs,
66            final int defStyle) {
67        super(context, attrs, defStyle);
68        mGestureDetector = new GestureDetector(context, this);
69        mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */);
70        mHandler = new Handler();
71    }
72
73    public void setOnKeyEventListener(final OnKeyEventListener listener) {
74        mListener = listener;
75    }
76
77    /**
78     * {@inheritDoc}
79     */
80    @Override
81    public void setKeyboard(final Keyboard keyboard) {
82        super.setKeyboard(keyboard);
83        mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
84        if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
85            if (mAccessibilityDelegate == null) {
86                mAccessibilityDelegate = new KeyboardAccessibilityDelegate<>(this, mKeyDetector);
87            }
88            mAccessibilityDelegate.setKeyboard(keyboard);
89        } else {
90            mAccessibilityDelegate = null;
91        }
92    }
93
94    @Override
95    public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) {
96        // Don't populate accessibility event with all Emoji keys.
97        return true;
98    }
99
100    /**
101     * {@inheritDoc}
102     */
103    @Override
104    public boolean onHoverEvent(final MotionEvent event) {
105        final KeyboardAccessibilityDelegate<EmojiPageKeyboardView> accessibilityDelegate =
106                mAccessibilityDelegate;
107        if (accessibilityDelegate != null
108                && AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
109            return accessibilityDelegate.onHoverEvent(event);
110        }
111        return super.onHoverEvent(event);
112    }
113
114    /**
115     * {@inheritDoc}
116     */
117    @Override
118    public boolean onTouchEvent(final MotionEvent e) {
119        if (mGestureDetector.onTouchEvent(e)) {
120            return true;
121        }
122        final Key key = getKey(e);
123        if (key != null && key != mCurrentKey) {
124            releaseCurrentKey(false /* withKeyRegistering */);
125        }
126        return true;
127    }
128
129    // {@link GestureEnabler#OnGestureListener} methods.
130    private Key mCurrentKey;
131    private Runnable mPendingKeyDown;
132    private final Handler mHandler;
133
134    private Key getKey(final MotionEvent e) {
135        final int index = e.getActionIndex();
136        final int x = (int)e.getX(index);
137        final int y = (int)e.getY(index);
138        return mKeyDetector.detectHitKey(x, y);
139    }
140
141    public void releaseCurrentKey(final boolean withKeyRegistering) {
142        mHandler.removeCallbacks(mPendingKeyDown);
143        mPendingKeyDown = null;
144        final Key currentKey = mCurrentKey;
145        if (currentKey == null) {
146            return;
147        }
148        currentKey.onReleased();
149        invalidateKey(currentKey);
150        if (withKeyRegistering) {
151            mListener.onReleaseKey(currentKey);
152        }
153        mCurrentKey = null;
154    }
155
156    @Override
157    public boolean onDown(final MotionEvent e) {
158        final Key key = getKey(e);
159        releaseCurrentKey(false /* withKeyRegistering */);
160        mCurrentKey = key;
161        if (key == null) {
162            return false;
163        }
164        // Do not trigger key-down effect right now in case this is actually a fling action.
165        mPendingKeyDown = new Runnable() {
166            @Override
167            public void run() {
168                mPendingKeyDown = null;
169                key.onPressed();
170                invalidateKey(key);
171                mListener.onPressKey(key);
172            }
173        };
174        mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME);
175        return false;
176    }
177
178    @Override
179    public void onShowPress(final MotionEvent e) {
180        // User feedback is done at {@link #onDown(MotionEvent)}.
181    }
182
183    @Override
184    public boolean onSingleTapUp(final MotionEvent e) {
185        final Key key = getKey(e);
186        final Runnable pendingKeyDown = mPendingKeyDown;
187        final Key currentKey = mCurrentKey;
188        releaseCurrentKey(false /* withKeyRegistering */);
189        if (key == null) {
190            return false;
191        }
192        if (key == currentKey && pendingKeyDown != null) {
193            pendingKeyDown.run();
194            // Trigger key-release event a little later so that a user can see visual feedback.
195            mHandler.postDelayed(new Runnable() {
196                @Override
197                public void run() {
198                    key.onReleased();
199                    invalidateKey(key);
200                    mListener.onReleaseKey(key);
201                }
202            }, KEY_RELEASE_DELAY_TIME);
203        } else {
204            key.onReleased();
205            invalidateKey(key);
206            mListener.onReleaseKey(key);
207        }
208        return true;
209    }
210
211    @Override
212    public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX,
213           final float distanceY) {
214        releaseCurrentKey(false /* withKeyRegistering */);
215        return false;
216    }
217
218    @Override
219    public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX,
220            final float velocityY) {
221        releaseCurrentKey(false /* withKeyRegistering */);
222        return false;
223    }
224
225    @Override
226    public void onLongPress(final MotionEvent e) {
227        // Long press detection of {@link #mGestureDetector} is disabled and not used.
228    }
229}
230