• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.inputmethod.keyboard.emoji;
18 
19 import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE;
20 
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.Color;
25 import android.os.CountDownTimer;
26 import android.preference.PreferenceManager;
27 import android.support.v4.view.ViewPager;
28 import android.util.AttributeSet;
29 import android.util.Pair;
30 import android.util.TypedValue;
31 import android.view.LayoutInflater;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.widget.ImageButton;
35 import android.widget.ImageView;
36 import android.widget.LinearLayout;
37 import android.widget.TabHost;
38 import android.widget.TabHost.OnTabChangeListener;
39 import android.widget.TabWidget;
40 import android.widget.TextView;
41 
42 import com.android.inputmethod.keyboard.Key;
43 import com.android.inputmethod.keyboard.KeyboardActionListener;
44 import com.android.inputmethod.keyboard.KeyboardLayoutSet;
45 import com.android.inputmethod.keyboard.KeyboardView;
46 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
47 import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
48 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
49 import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
50 import com.android.inputmethod.latin.Constants;
51 import com.android.inputmethod.latin.R;
52 import com.android.inputmethod.latin.SubtypeSwitcher;
53 import com.android.inputmethod.latin.utils.ResourceUtils;
54 
55 import java.util.concurrent.TimeUnit;
56 
57 /**
58  * View class to implement Emoji palettes.
59  * The Emoji keyboard consists of group of views layout/emoji_palettes_view.
60  * <ol>
61  * <li> Emoji category tabs.
62  * <li> Delete button.
63  * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab.
64  * <li> Back to main keyboard button and enter button.
65  * </ol>
66  * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
67  */
68 public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener,
69         ViewPager.OnPageChangeListener, View.OnClickListener, View.OnTouchListener,
70         EmojiPageKeyboardView.OnKeyEventListener {
71     private final int mFunctionalKeyBackgroundId;
72     private final int mSpacebarBackgroundId;
73     private final boolean mCategoryIndicatorEnabled;
74     private final int mCategoryIndicatorDrawableResId;
75     private final int mCategoryIndicatorBackgroundResId;
76     private final int mCategoryPageIndicatorColor;
77     private final int mCategoryPageIndicatorBackground;
78     private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener;
79     private EmojiPalettesAdapter mEmojiPalettesAdapter;
80     private final EmojiLayoutParams mEmojiLayoutParams;
81 
82     private ImageButton mDeleteKey;
83     private TextView mAlphabetKeyLeft;
84     private TextView mAlphabetKeyRight;
85     private View mSpacebar;
86     // TODO: Remove this workaround.
87     private View mSpacebarIcon;
88     private TabHost mTabHost;
89     private ViewPager mEmojiPager;
90     private int mCurrentPagerPosition = 0;
91     private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
92 
93     private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
94 
95     private final EmojiCategory mEmojiCategory;
96 
EmojiPalettesView(final Context context, final AttributeSet attrs)97     public EmojiPalettesView(final Context context, final AttributeSet attrs) {
98         this(context, attrs, R.attr.emojiPalettesViewStyle);
99     }
100 
EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle)101     public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) {
102         super(context, attrs, defStyle);
103         final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
104                 R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
105         final int keyBackgroundId = keyboardViewAttr.getResourceId(
106                 R.styleable.KeyboardView_keyBackground, 0);
107         mFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId(
108                 R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId);
109         mSpacebarBackgroundId = keyboardViewAttr.getResourceId(
110                 R.styleable.KeyboardView_spacebarBackground, keyBackgroundId);
111         keyboardViewAttr.recycle();
112         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
113                 context, null /* editorInfo */);
114         final Resources res = context.getResources();
115         mEmojiLayoutParams = new EmojiLayoutParams(res);
116         builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype());
117         builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
118                 mEmojiLayoutParams.mEmojiKeyboardHeight);
119         final KeyboardLayoutSet layoutSet = builder.build();
120         final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs,
121                 R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView);
122         mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context),
123                 res, layoutSet, emojiPalettesViewAttr);
124         mCategoryIndicatorEnabled = emojiPalettesViewAttr.getBoolean(
125                 R.styleable.EmojiPalettesView_categoryIndicatorEnabled, false);
126         mCategoryIndicatorDrawableResId = emojiPalettesViewAttr.getResourceId(
127                 R.styleable.EmojiPalettesView_categoryIndicatorDrawable, 0);
128         mCategoryIndicatorBackgroundResId = emojiPalettesViewAttr.getResourceId(
129                 R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0);
130         mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor(
131                 R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0);
132         mCategoryPageIndicatorBackground = emojiPalettesViewAttr.getColor(
133                 R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0);
134         emojiPalettesViewAttr.recycle();
135         mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(context);
136     }
137 
138     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)139     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
140         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
141         final Resources res = getContext().getResources();
142         // The main keyboard expands to the entire this {@link KeyboardView}.
143         final int width = ResourceUtils.getDefaultKeyboardWidth(res)
144                 + getPaddingLeft() + getPaddingRight();
145         final int height = ResourceUtils.getDefaultKeyboardHeight(res)
146                 + res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height)
147                 + getPaddingTop() + getPaddingBottom();
148         setMeasuredDimension(width, height);
149     }
150 
addTab(final TabHost host, final int categoryId)151     private void addTab(final TabHost host, final int categoryId) {
152         final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */);
153         final TabHost.TabSpec tspec = host.newTabSpec(tabId);
154         tspec.setContent(R.id.emoji_keyboard_dummy);
155         final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate(
156                 R.layout.emoji_keyboard_tab_icon, null);
157         iconView.setImageResource(mEmojiCategory.getCategoryTabIcon(categoryId));
158         iconView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId));
159         tspec.setIndicator(iconView);
160         host.addTab(tspec);
161     }
162 
163     @Override
onFinishInflate()164     protected void onFinishInflate() {
165         mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost);
166         mTabHost.setup();
167         for (final EmojiCategory.CategoryProperties properties
168                 : mEmojiCategory.getShownCategories()) {
169             addTab(mTabHost, properties.mCategoryId);
170         }
171         mTabHost.setOnTabChangedListener(this);
172         final TabWidget tabWidget = mTabHost.getTabWidget();
173         tabWidget.setStripEnabled(mCategoryIndicatorEnabled);
174         if (mCategoryIndicatorEnabled) {
175             // On TabWidget's strip, what looks like an indicator is actually a background.
176             // And what looks like a background are actually left and right drawables.
177             tabWidget.setBackgroundResource(mCategoryIndicatorDrawableResId);
178             tabWidget.setLeftStripDrawable(mCategoryIndicatorBackgroundResId);
179             tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId);
180         }
181 
182         mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this);
183 
184         mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager);
185         mEmojiPager.setAdapter(mEmojiPalettesAdapter);
186         mEmojiPager.setOnPageChangeListener(this);
187         mEmojiPager.setOffscreenPageLimit(0);
188         mEmojiPager.setPersistentDrawingCache(PERSISTENT_NO_CACHE);
189         mEmojiLayoutParams.setPagerProperties(mEmojiPager);
190 
191         mEmojiCategoryPageIndicatorView =
192                 (EmojiCategoryPageIndicatorView)findViewById(R.id.emoji_category_page_id_view);
193         mEmojiCategoryPageIndicatorView.setColors(
194                 mCategoryPageIndicatorColor, mCategoryPageIndicatorBackground);
195         mEmojiLayoutParams.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView);
196 
197         setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */);
198 
199         final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar);
200         mEmojiLayoutParams.setActionBarProperties(actionBar);
201 
202         // deleteKey depends only on OnTouchListener.
203         mDeleteKey = (ImageButton)findViewById(R.id.emoji_keyboard_delete);
204         mDeleteKey.setBackgroundResource(mFunctionalKeyBackgroundId);
205         mDeleteKey.setTag(Constants.CODE_DELETE);
206         mDeleteKey.setOnTouchListener(mDeleteKeyOnTouchListener);
207 
208         // {@link #mAlphabetKeyLeft}, {@link #mAlphabetKeyRight, and spaceKey depend on
209         // {@link View.OnClickListener} as well as {@link View.OnTouchListener}.
210         // {@link View.OnTouchListener} is used as the trigger of key-press, while
211         // {@link View.OnClickListener} is used as the trigger of key-release which does not occur
212         // if the event is canceled by moving off the finger from the view.
213         // The text on alphabet keys are set at
214         // {@link #startEmojiPalettes(String,int,float,Typeface)}.
215         mAlphabetKeyLeft = (TextView)findViewById(R.id.emoji_keyboard_alphabet_left);
216         mAlphabetKeyLeft.setBackgroundResource(mFunctionalKeyBackgroundId);
217         mAlphabetKeyLeft.setTag(Constants.CODE_ALPHA_FROM_EMOJI);
218         mAlphabetKeyLeft.setOnTouchListener(this);
219         mAlphabetKeyLeft.setOnClickListener(this);
220         mAlphabetKeyRight = (TextView)findViewById(R.id.emoji_keyboard_alphabet_right);
221         mAlphabetKeyRight.setBackgroundResource(mFunctionalKeyBackgroundId);
222         mAlphabetKeyRight.setTag(Constants.CODE_ALPHA_FROM_EMOJI);
223         mAlphabetKeyRight.setOnTouchListener(this);
224         mAlphabetKeyRight.setOnClickListener(this);
225         mSpacebar = findViewById(R.id.emoji_keyboard_space);
226         mSpacebar.setBackgroundResource(mSpacebarBackgroundId);
227         mSpacebar.setTag(Constants.CODE_SPACE);
228         mSpacebar.setOnTouchListener(this);
229         mSpacebar.setOnClickListener(this);
230         mEmojiLayoutParams.setKeyProperties(mSpacebar);
231         mSpacebarIcon = findViewById(R.id.emoji_keyboard_space_icon);
232     }
233 
234     @Override
dispatchTouchEvent(final MotionEvent ev)235     public boolean dispatchTouchEvent(final MotionEvent ev) {
236         // Add here to the stack trace to nail down the {@link IllegalArgumentException} exception
237         // in MotionEvent that sporadically happens.
238         // TODO: Remove this override method once the issue has been addressed.
239         return super.dispatchTouchEvent(ev);
240     }
241 
242     @Override
onTabChanged(final String tabId)243     public void onTabChanged(final String tabId) {
244         AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
245                 Constants.CODE_UNSPECIFIED, this);
246         final int categoryId = mEmojiCategory.getCategoryId(tabId);
247         setCurrentCategoryId(categoryId, false /* force */);
248         updateEmojiCategoryPageIdView();
249     }
250 
251     @Override
onPageSelected(final int position)252     public void onPageSelected(final int position) {
253         final Pair<Integer, Integer> newPos =
254                 mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
255         setCurrentCategoryId(newPos.first /* categoryId */, false /* force */);
256         mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */);
257         updateEmojiCategoryPageIdView();
258         mCurrentPagerPosition = position;
259     }
260 
261     @Override
onPageScrollStateChanged(final int state)262     public void onPageScrollStateChanged(final int state) {
263         // Ignore this message. Only want the actual page selected.
264     }
265 
266     @Override
onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels)267     public void onPageScrolled(final int position, final float positionOffset,
268             final int positionOffsetPixels) {
269         mEmojiPalettesAdapter.onPageScrolled();
270         final Pair<Integer, Integer> newPos =
271                 mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
272         final int newCategoryId = newPos.first;
273         final int newCategorySize = mEmojiCategory.getCategoryPageSize(newCategoryId);
274         final int currentCategoryId = mEmojiCategory.getCurrentCategoryId();
275         final int currentCategoryPageId = mEmojiCategory.getCurrentCategoryPageId();
276         final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageSize();
277         if (newCategoryId == currentCategoryId) {
278             mEmojiCategoryPageIndicatorView.setCategoryPageId(
279                     newCategorySize, newPos.second, positionOffset);
280         } else if (newCategoryId > currentCategoryId) {
281             mEmojiCategoryPageIndicatorView.setCategoryPageId(
282                     currentCategorySize, currentCategoryPageId, positionOffset);
283         } else if (newCategoryId < currentCategoryId) {
284             mEmojiCategoryPageIndicatorView.setCategoryPageId(
285                     currentCategorySize, currentCategoryPageId, positionOffset - 1);
286         }
287     }
288 
289     /**
290      * Called from {@link EmojiPageKeyboardView} through {@link android.view.View.OnTouchListener}
291      * interface to handle touch events from View-based elements such as the space bar.
292      * Note that this method is used only for observing {@link MotionEvent#ACTION_DOWN} to trigger
293      * {@link KeyboardActionListener#onPressKey}. {@link KeyboardActionListener#onReleaseKey} will
294      * be covered by {@link #onClick} as long as the event is not canceled.
295      */
296     @Override
onTouch(final View v, final MotionEvent event)297     public boolean onTouch(final View v, final MotionEvent event) {
298         if (event.getActionMasked() != MotionEvent.ACTION_DOWN) {
299             return false;
300         }
301         final Object tag = v.getTag();
302         if (!(tag instanceof Integer)) {
303             return false;
304         }
305         final int code = (Integer) tag;
306         mKeyboardActionListener.onPressKey(
307                 code, 0 /* repeatCount */, true /* isSinglePointer */);
308         // It's important to return false here. Otherwise, {@link #onClick} and touch-down visual
309         // feedback stop working.
310         return false;
311     }
312 
313     /**
314      * Called from {@link EmojiPageKeyboardView} through {@link android.view.View.OnClickListener}
315      * interface to handle non-canceled touch-up events from View-based elements such as the space
316      * bar.
317      */
318     @Override
onClick(View v)319     public void onClick(View v) {
320         final Object tag = v.getTag();
321         if (!(tag instanceof Integer)) {
322             return;
323         }
324         final int code = (Integer) tag;
325         mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE,
326                 false /* isKeyRepeat */);
327         mKeyboardActionListener.onReleaseKey(code, false /* withSliding */);
328     }
329 
330     /**
331      * Called from {@link EmojiPageKeyboardView} through
332      * {@link com.android.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener}
333      * interface to handle touch events from non-View-based elements such as Emoji buttons.
334      */
335     @Override
onPressKey(final Key key)336     public void onPressKey(final Key key) {
337         final int code = key.getCode();
338         mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */);
339     }
340 
341     /**
342      * Called from {@link EmojiPageKeyboardView} through
343      * {@link com.android.inputmethod.keyboard.emoji.EmojiPageKeyboardView.OnKeyEventListener}
344      * interface to handle touch events from non-View-based elements such as Emoji buttons.
345      */
346     @Override
onReleaseKey(final Key key)347     public void onReleaseKey(final Key key) {
348         mEmojiPalettesAdapter.addRecentKey(key);
349         mEmojiCategory.saveLastTypedCategoryPage();
350         final int code = key.getCode();
351         if (code == Constants.CODE_OUTPUT_TEXT) {
352             mKeyboardActionListener.onTextInput(key.getOutputText());
353         } else {
354             mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE,
355                     false /* isKeyRepeat */);
356         }
357         mKeyboardActionListener.onReleaseKey(code, false /* withSliding */);
358     }
359 
setHardwareAcceleratedDrawingEnabled(final boolean enabled)360     public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
361         if (!enabled) return;
362         // TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off?
363         setLayerType(LAYER_TYPE_HARDWARE, null);
364     }
365 
setupAlphabetKey(final TextView alphabetKey, final String label, final KeyDrawParams params)366     private static void setupAlphabetKey(final TextView alphabetKey, final String label,
367             final KeyDrawParams params) {
368         alphabetKey.setText(label);
369         alphabetKey.setTextColor(params.mFunctionalTextColor);
370         alphabetKey.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mLabelSize);
371         alphabetKey.setTypeface(params.mTypeface);
372     }
373 
startEmojiPalettes(final String switchToAlphaLabel, final KeyVisualAttributes keyVisualAttr, final KeyboardIconsSet iconSet)374     public void startEmojiPalettes(final String switchToAlphaLabel,
375             final KeyVisualAttributes keyVisualAttr, final KeyboardIconsSet iconSet) {
376         final int deleteIconResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_DELETE_KEY);
377         if (deleteIconResId != 0) {
378             mDeleteKey.setImageResource(deleteIconResId);
379         }
380         final int spacebarResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_SPACE_KEY);
381         if (spacebarResId != 0) {
382             // TODO: Remove this workaround to place the spacebar icon.
383             mSpacebarIcon.setBackgroundResource(spacebarResId);
384         }
385         final KeyDrawParams params = new KeyDrawParams();
386         params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr);
387         setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params);
388         setupAlphabetKey(mAlphabetKeyRight, switchToAlphaLabel, params);
389         mEmojiPager.setAdapter(mEmojiPalettesAdapter);
390         mEmojiPager.setCurrentItem(mCurrentPagerPosition);
391     }
392 
stopEmojiPalettes()393     public void stopEmojiPalettes() {
394         mEmojiPalettesAdapter.releaseCurrentKey(true /* withKeyRegistering */);
395         mEmojiPalettesAdapter.flushPendingRecentKeys();
396         mEmojiPager.setAdapter(null);
397     }
398 
setKeyboardActionListener(final KeyboardActionListener listener)399     public void setKeyboardActionListener(final KeyboardActionListener listener) {
400         mKeyboardActionListener = listener;
401         mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener);
402     }
403 
updateEmojiCategoryPageIdView()404     private void updateEmojiCategoryPageIdView() {
405         if (mEmojiCategoryPageIndicatorView == null) {
406             return;
407         }
408         mEmojiCategoryPageIndicatorView.setCategoryPageId(
409                 mEmojiCategory.getCurrentCategoryPageSize(),
410                 mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */);
411     }
412 
setCurrentCategoryId(final int categoryId, final boolean force)413     private void setCurrentCategoryId(final int categoryId, final boolean force) {
414         final int oldCategoryId = mEmojiCategory.getCurrentCategoryId();
415         if (oldCategoryId == categoryId && !force) {
416             return;
417         }
418 
419         if (oldCategoryId == EmojiCategory.ID_RECENTS) {
420             // Needs to save pending updates for recent keys when we get out of the recents
421             // category because we don't want to move the recent emojis around while the user
422             // is in the recents category.
423             mEmojiPalettesAdapter.flushPendingRecentKeys();
424         }
425 
426         mEmojiCategory.setCurrentCategoryId(categoryId);
427         final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId);
428         final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId);
429         if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(
430                 mEmojiPager.getCurrentItem()).first != categoryId) {
431             mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */);
432         }
433         if (force || mTabHost.getCurrentTab() != newTabId) {
434             mTabHost.setCurrentTab(newTabId);
435         }
436     }
437 
438     private static class DeleteKeyOnTouchListener implements OnTouchListener {
439         static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30);
440         final long mKeyRepeatStartTimeout;
441         final long mKeyRepeatInterval;
442 
DeleteKeyOnTouchListener(Context context)443         public DeleteKeyOnTouchListener(Context context) {
444             final Resources res = context.getResources();
445             mKeyRepeatStartTimeout = res.getInteger(R.integer.config_key_repeat_start_timeout);
446             mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
447             mTimer = new CountDownTimer(MAX_REPEAT_COUNT_TIME, mKeyRepeatInterval) {
448                 @Override
449                 public void onTick(long millisUntilFinished) {
450                     final long elapsed = MAX_REPEAT_COUNT_TIME - millisUntilFinished;
451                     if (elapsed < mKeyRepeatStartTimeout) {
452                         return;
453                     }
454                     onKeyRepeat();
455                 }
456                 @Override
457                 public void onFinish() {
458                     onKeyRepeat();
459                 }
460             };
461         }
462 
463         /** Key-repeat state. */
464         private static final int KEY_REPEAT_STATE_INITIALIZED = 0;
465         // The key is touched but auto key-repeat is not started yet.
466         private static final int KEY_REPEAT_STATE_KEY_DOWN = 1;
467         // At least one key-repeat event has already been triggered and the key is not released.
468         private static final int KEY_REPEAT_STATE_KEY_REPEAT = 2;
469 
470         private KeyboardActionListener mKeyboardActionListener =
471                 KeyboardActionListener.EMPTY_LISTENER;
472 
473         // TODO: Do the same things done in PointerTracker
474         private final CountDownTimer mTimer;
475         private int mState = KEY_REPEAT_STATE_INITIALIZED;
476         private int mRepeatCount = 0;
477 
setKeyboardActionListener(final KeyboardActionListener listener)478         public void setKeyboardActionListener(final KeyboardActionListener listener) {
479             mKeyboardActionListener = listener;
480         }
481 
482         @Override
onTouch(final View v, final MotionEvent event)483         public boolean onTouch(final View v, final MotionEvent event) {
484             switch (event.getActionMasked()) {
485             case MotionEvent.ACTION_DOWN:
486                 onTouchDown(v);
487                 return true;
488             case MotionEvent.ACTION_MOVE:
489                 final float x = event.getX();
490                 final float y = event.getY();
491                 if (x < 0.0f || v.getWidth() < x || y < 0.0f || v.getHeight() < y) {
492                     // Stop generating key events once the finger moves away from the view area.
493                     onTouchCanceled(v);
494                 }
495                 return true;
496             case MotionEvent.ACTION_CANCEL:
497             case MotionEvent.ACTION_UP:
498                 onTouchUp(v);
499                 return true;
500             }
501             return false;
502         }
503 
handleKeyDown()504         private void handleKeyDown() {
505             mKeyboardActionListener.onPressKey(
506                     Constants.CODE_DELETE, mRepeatCount, true /* isSinglePointer */);
507         }
508 
handleKeyUp()509         private void handleKeyUp() {
510             mKeyboardActionListener.onCodeInput(Constants.CODE_DELETE,
511                     NOT_A_COORDINATE, NOT_A_COORDINATE, false /* isKeyRepeat */);
512             mKeyboardActionListener.onReleaseKey(
513                     Constants.CODE_DELETE, false /* withSliding */);
514             ++mRepeatCount;
515         }
516 
onTouchDown(final View v)517         private void onTouchDown(final View v) {
518             mTimer.cancel();
519             mRepeatCount = 0;
520             handleKeyDown();
521             v.setPressed(true /* pressed */);
522             mState = KEY_REPEAT_STATE_KEY_DOWN;
523             mTimer.start();
524         }
525 
onTouchUp(final View v)526         private void onTouchUp(final View v) {
527             mTimer.cancel();
528             if (mState == KEY_REPEAT_STATE_KEY_DOWN) {
529                 handleKeyUp();
530             }
531             v.setPressed(false /* pressed */);
532             mState = KEY_REPEAT_STATE_INITIALIZED;
533         }
534 
onTouchCanceled(final View v)535         private void onTouchCanceled(final View v) {
536             mTimer.cancel();
537             v.setBackgroundColor(Color.TRANSPARENT);
538             mState = KEY_REPEAT_STATE_INITIALIZED;
539         }
540 
541         // Called by {@link #mTimer} in the UI thread as an auto key-repeat signal.
onKeyRepeat()542         void onKeyRepeat() {
543             switch (mState) {
544             case KEY_REPEAT_STATE_INITIALIZED:
545                 // Basically this should not happen.
546                 break;
547             case KEY_REPEAT_STATE_KEY_DOWN:
548                 // Do not call {@link #handleKeyDown} here because it has already been called
549                 // in {@link #onTouchDown}.
550                 handleKeyUp();
551                 mState = KEY_REPEAT_STATE_KEY_REPEAT;
552                 break;
553             case KEY_REPEAT_STATE_KEY_REPEAT:
554                 handleKeyDown();
555                 handleKeyUp();
556                 break;
557             }
558         }
559     }
560 }
561