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 static com.android.inputmethod.latin.common.Constants.NOT_A_COORDINATE;
20
21import android.content.Context;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.graphics.Color;
25import android.preference.PreferenceManager;
26import android.support.v4.view.ViewPager;
27import android.util.AttributeSet;
28import android.util.Pair;
29import android.util.TypedValue;
30import android.view.LayoutInflater;
31import android.view.MotionEvent;
32import android.view.View;
33import android.widget.ImageButton;
34import android.widget.ImageView;
35import android.widget.LinearLayout;
36import android.widget.TabHost;
37import android.widget.TabHost.OnTabChangeListener;
38import android.widget.TabWidget;
39import android.widget.TextView;
40
41import com.android.inputmethod.keyboard.Key;
42import com.android.inputmethod.keyboard.KeyboardActionListener;
43import com.android.inputmethod.keyboard.KeyboardLayoutSet;
44import com.android.inputmethod.keyboard.KeyboardView;
45import com.android.inputmethod.keyboard.internal.KeyDrawParams;
46import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
47import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
48import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
49import com.android.inputmethod.latin.R;
50import com.android.inputmethod.latin.RichInputMethodSubtype;
51import com.android.inputmethod.latin.common.Constants;
52import com.android.inputmethod.latin.utils.ResourceUtils;
53
54/**
55 * View class to implement Emoji palettes.
56 * The Emoji keyboard consists of group of views layout/emoji_palettes_view.
57 * <ol>
58 * <li> Emoji category tabs.
59 * <li> Delete button.
60 * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab.
61 * <li> Back to main keyboard button and enter button.
62 * </ol>
63 * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
64 */
65public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener,
66        ViewPager.OnPageChangeListener, View.OnClickListener, View.OnTouchListener,
67        EmojiPageKeyboardView.OnKeyEventListener {
68    private final int mFunctionalKeyBackgroundId;
69    private final int mSpacebarBackgroundId;
70    private final boolean mCategoryIndicatorEnabled;
71    private final int mCategoryIndicatorDrawableResId;
72    private final int mCategoryIndicatorBackgroundResId;
73    private final int mCategoryPageIndicatorColor;
74    private final int mCategoryPageIndicatorBackground;
75    private EmojiPalettesAdapter mEmojiPalettesAdapter;
76    private final EmojiLayoutParams mEmojiLayoutParams;
77    private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener;
78
79    private ImageButton mDeleteKey;
80    private TextView mAlphabetKeyLeft;
81    private TextView mAlphabetKeyRight;
82    private View mSpacebar;
83    // TODO: Remove this workaround.
84    private View mSpacebarIcon;
85    private TabHost mTabHost;
86    private ViewPager mEmojiPager;
87    private int mCurrentPagerPosition = 0;
88    private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
89
90    private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
91
92    private final EmojiCategory mEmojiCategory;
93
94    public EmojiPalettesView(final Context context, final AttributeSet attrs) {
95        this(context, attrs, R.attr.emojiPalettesViewStyle);
96    }
97
98    public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) {
99        super(context, attrs, defStyle);
100        final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
101                R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
102        final int keyBackgroundId = keyboardViewAttr.getResourceId(
103                R.styleable.KeyboardView_keyBackground, 0);
104        mFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId(
105                R.styleable.KeyboardView_functionalKeyBackground, keyBackgroundId);
106        mSpacebarBackgroundId = keyboardViewAttr.getResourceId(
107                R.styleable.KeyboardView_spacebarBackground, keyBackgroundId);
108        keyboardViewAttr.recycle();
109        final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
110                context, null /* editorInfo */);
111        final Resources res = context.getResources();
112        mEmojiLayoutParams = new EmojiLayoutParams(res);
113        builder.setSubtype(RichInputMethodSubtype.getEmojiSubtype());
114        builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
115                mEmojiLayoutParams.mEmojiKeyboardHeight);
116        final KeyboardLayoutSet layoutSet = builder.build();
117        final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs,
118                R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView);
119        mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context),
120                res, layoutSet, emojiPalettesViewAttr);
121        mCategoryIndicatorEnabled = emojiPalettesViewAttr.getBoolean(
122                R.styleable.EmojiPalettesView_categoryIndicatorEnabled, false);
123        mCategoryIndicatorDrawableResId = emojiPalettesViewAttr.getResourceId(
124                R.styleable.EmojiPalettesView_categoryIndicatorDrawable, 0);
125        mCategoryIndicatorBackgroundResId = emojiPalettesViewAttr.getResourceId(
126                R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0);
127        mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor(
128                R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0);
129        mCategoryPageIndicatorBackground = emojiPalettesViewAttr.getColor(
130                R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0);
131        emojiPalettesViewAttr.recycle();
132        mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener();
133    }
134
135    @Override
136    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
137        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
138        final Resources res = getContext().getResources();
139        // The main keyboard expands to the entire this {@link KeyboardView}.
140        final int width = ResourceUtils.getDefaultKeyboardWidth(res)
141                + getPaddingLeft() + getPaddingRight();
142        final int height = ResourceUtils.getDefaultKeyboardHeight(res)
143                + res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height)
144                + getPaddingTop() + getPaddingBottom();
145        setMeasuredDimension(width, height);
146    }
147
148    private void addTab(final TabHost host, final int categoryId) {
149        final String tabId = EmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */);
150        final TabHost.TabSpec tspec = host.newTabSpec(tabId);
151        tspec.setContent(R.id.emoji_keyboard_dummy);
152        final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate(
153                R.layout.emoji_keyboard_tab_icon, null);
154        // TODO: Replace background color with its own setting rather than using the
155        //       category page indicator background as a workaround.
156        iconView.setBackgroundColor(mCategoryPageIndicatorBackground);
157        iconView.setImageResource(mEmojiCategory.getCategoryTabIcon(categoryId));
158        iconView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId));
159        tspec.setIndicator(iconView);
160        host.addTab(tspec);
161    }
162
163    @Override
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
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
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
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
262    public void onPageScrollStateChanged(final int state) {
263        // Ignore this message. Only want the actual page selected.
264    }
265
266    @Override
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
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
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
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
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
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
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
374    public void startEmojiPalettes(final String switchToAlphaLabel,
375                                   final KeyVisualAttributes keyVisualAttr,
376                                   final KeyboardIconsSet iconSet) {
377        final int deleteIconResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_DELETE_KEY);
378        if (deleteIconResId != 0) {
379            mDeleteKey.setImageResource(deleteIconResId);
380        }
381        final int spacebarResId = iconSet.getIconResourceId(KeyboardIconsSet.NAME_SPACE_KEY);
382        if (spacebarResId != 0) {
383            // TODO: Remove this workaround to place the spacebar icon.
384            mSpacebarIcon.setBackgroundResource(spacebarResId);
385        }
386        final KeyDrawParams params = new KeyDrawParams();
387        params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr);
388        setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params);
389        setupAlphabetKey(mAlphabetKeyRight, switchToAlphaLabel, params);
390        mEmojiPager.setAdapter(mEmojiPalettesAdapter);
391        mEmojiPager.setCurrentItem(mCurrentPagerPosition);
392    }
393
394    public void stopEmojiPalettes() {
395        mEmojiPalettesAdapter.releaseCurrentKey(true /* withKeyRegistering */);
396        mEmojiPalettesAdapter.flushPendingRecentKeys();
397        mEmojiPager.setAdapter(null);
398    }
399
400    public void setKeyboardActionListener(final KeyboardActionListener listener) {
401        mKeyboardActionListener = listener;
402        mDeleteKeyOnTouchListener.setKeyboardActionListener(listener);
403    }
404
405    private void updateEmojiCategoryPageIdView() {
406        if (mEmojiCategoryPageIndicatorView == null) {
407            return;
408        }
409        mEmojiCategoryPageIndicatorView.setCategoryPageId(
410                mEmojiCategory.getCurrentCategoryPageSize(),
411                mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */);
412    }
413
414    private void setCurrentCategoryId(final int categoryId, final boolean force) {
415        final int oldCategoryId = mEmojiCategory.getCurrentCategoryId();
416        if (oldCategoryId == categoryId && !force) {
417            return;
418        }
419
420        if (oldCategoryId == EmojiCategory.ID_RECENTS) {
421            // Needs to save pending updates for recent keys when we get out of the recents
422            // category because we don't want to move the recent emojis around while the user
423            // is in the recents category.
424            mEmojiPalettesAdapter.flushPendingRecentKeys();
425        }
426
427        mEmojiCategory.setCurrentCategoryId(categoryId);
428        final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId);
429        final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId);
430        if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(
431                mEmojiPager.getCurrentItem()).first != categoryId) {
432            mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */);
433        }
434        if (force || mTabHost.getCurrentTab() != newTabId) {
435            mTabHost.setCurrentTab(newTabId);
436        }
437    }
438
439    private static class DeleteKeyOnTouchListener implements OnTouchListener {
440        private KeyboardActionListener mKeyboardActionListener =
441                KeyboardActionListener.EMPTY_LISTENER;
442
443        public void setKeyboardActionListener(final KeyboardActionListener listener) {
444            mKeyboardActionListener = listener;
445        }
446
447        @Override
448        public boolean onTouch(final View v, final MotionEvent event) {
449            switch (event.getActionMasked()) {
450                case MotionEvent.ACTION_DOWN:
451                    onTouchDown(v);
452                    return true;
453                case MotionEvent.ACTION_MOVE:
454                    final float x = event.getX();
455                    final float y = event.getY();
456                    if (x < 0.0f || v.getWidth() < x || y < 0.0f || v.getHeight() < y) {
457                        // Stop generating key events once the finger moves away from the view area.
458                        onTouchCanceled(v);
459                    }
460                    return true;
461                case MotionEvent.ACTION_CANCEL:
462                case MotionEvent.ACTION_UP:
463                    onTouchUp(v);
464                    return true;
465            }
466            return false;
467        }
468
469        private void onTouchDown(final View v) {
470            mKeyboardActionListener.onPressKey(Constants.CODE_DELETE,
471                    0 /* repeatCount */, true /* isSinglePointer */);
472            v.setPressed(true /* pressed */);
473        }
474
475        private void onTouchUp(final View v) {
476            mKeyboardActionListener.onCodeInput(Constants.CODE_DELETE,
477                    NOT_A_COORDINATE, NOT_A_COORDINATE, false /* isKeyRepeat */);
478            mKeyboardActionListener.onReleaseKey(Constants.CODE_DELETE, false /* withSliding */);
479            v.setPressed(false /* pressed */);
480        }
481
482        private void onTouchCanceled(final View v) {
483            v.setBackgroundColor(Color.TRANSPARENT);
484        }
485    }
486}