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    void callListenerOnReleaseKey(final Key releasedKey, final boolean withKeyRegistering) {
142        releasedKey.onReleased();
143        invalidateKey(releasedKey);
144        if (withKeyRegistering) {
145            mListener.onReleaseKey(releasedKey);
146        }
147    }
148
149    void callListenerOnPressKey(final Key pressedKey) {
150        mPendingKeyDown = null;
151        pressedKey.onPressed();
152        invalidateKey(pressedKey);
153        mListener.onPressKey(pressedKey);
154    }
155
156    public void releaseCurrentKey(final boolean withKeyRegistering) {
157        mHandler.removeCallbacks(mPendingKeyDown);
158        mPendingKeyDown = null;
159        final Key currentKey = mCurrentKey;
160        if (currentKey == null) {
161            return;
162        }
163        callListenerOnReleaseKey(currentKey, withKeyRegistering);
164        mCurrentKey = null;
165    }
166
167    @Override
168    public boolean onDown(final MotionEvent e) {
169        final Key key = getKey(e);
170        releaseCurrentKey(false /* withKeyRegistering */);
171        mCurrentKey = key;
172        if (key == null) {
173            return false;
174        }
175        // Do not trigger key-down effect right now in case this is actually a fling action.
176        mPendingKeyDown = new Runnable() {
177            @Override
178            public void run() {
179                callListenerOnPressKey(key);
180            }
181        };
182        mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME);
183        return false;
184    }
185
186    @Override
187    public void onShowPress(final MotionEvent e) {
188        // User feedback is done at {@link #onDown(MotionEvent)}.
189    }
190
191    @Override
192    public boolean onSingleTapUp(final MotionEvent e) {
193        final Key key = getKey(e);
194        final Runnable pendingKeyDown = mPendingKeyDown;
195        final Key currentKey = mCurrentKey;
196        releaseCurrentKey(false /* withKeyRegistering */);
197        if (key == null) {
198            return false;
199        }
200        if (key == currentKey && pendingKeyDown != null) {
201            pendingKeyDown.run();
202            // Trigger key-release event a little later so that a user can see visual feedback.
203            mHandler.postDelayed(new Runnable() {
204                @Override
205                public void run() {
206                    callListenerOnReleaseKey(key, true /* withRegistering */);
207                }
208            }, KEY_RELEASE_DELAY_TIME);
209        } else {
210            callListenerOnReleaseKey(key, true /* withRegistering */);
211        }
212        return true;
213    }
214
215    @Override
216    public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX,
217           final float distanceY) {
218        releaseCurrentKey(false /* withKeyRegistering */);
219        return false;
220    }
221
222    @Override
223    public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX,
224            final float velocityY) {
225        releaseCurrentKey(false /* withKeyRegistering */);
226        return false;
227    }
228
229    @Override
230    public void onLongPress(final MotionEvent e) {
231        // Long press detection of {@link #mGestureDetector} is disabled and not used.
232    }
233}
234