1 /*
2  * Copyright (C) 2019 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.leanback;
18 
19 import android.content.Context;
20 
21 import java.util.ArrayList;
22 
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.content.res.XmlResourceParser;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.Paint;
29 import android.graphics.Paint.Align;
30 import android.graphics.Rect;
31 import android.graphics.Typeface;
32 import android.inputmethodservice.Keyboard;
33 import android.inputmethodservice.Keyboard.Key;
34 import android.inputmethodservice.Keyboard.Row;
35 import android.media.AudioManager;
36 import android.provider.Settings;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.accessibility.AccessibilityEvent;
42 import android.view.accessibility.AccessibilityManager;
43 import android.widget.FrameLayout;
44 import android.widget.ImageView;
45 
46 import java.util.HashMap;
47 import java.util.Iterator;
48 import java.util.List;
49 import java.util.Map;
50 
51 public class LeanbackKeyboardView extends FrameLayout {
52 
53     private static final String TAG = "LbKbView";
54     private static final boolean DEBUG = false;
55 
56     private static final int NOT_A_KEY = -1;
57 
58     public static final int SHIFT_OFF = 0;
59     public static final int SHIFT_ON = 1;
60     public static final int SHIFT_LOCKED = 2;
61     private int mShiftState;
62 
63     private final float mFocusedScale;
64     private final float mClickedScale;
65     private final int mClickAnimDur;
66     private final int mUnfocusStartDelay;
67     private final int mInactiveMiniKbAlpha;
68 
69     private Keyboard mKeyboard;
70     private KeyHolder[] mKeys;
71     private ImageView[] mKeyImageViews;
72 
73     private int mFocusIndex;
74     private boolean mFocusClicked;
75     private View mCurrentFocusView;
76     private boolean mMiniKeyboardOnScreen;
77 
78     /**
79      * Special keycodes
80      */
81     public static final int ASCII_SPACE = 32;
82     public static final int ASCII_PERIOD = 46;
83     public static final int KEYCODE_SHIFT = -1;
84     public static final int KEYCODE_SYM_TOGGLE = -2;
85     public static final int KEYCODE_LEFT = -3;
86     public static final int KEYCODE_RIGHT = -4;
87     public static final int KEYCODE_DELETE = -5;
88     public static final int KEYCODE_CAPS_LOCK = -6;
89     public static final int KEYCODE_VOICE = -7;
90     public static final int KEYCODE_DISMISS_MINI_KEYBOARD = -8;
91 
92     private int mBaseMiniKbIndex = -1;
93 
94     private Paint mPaint;
95     private Rect mPadding;
96     private int mModeChangeTextSize;
97     private int mKeyTextSize;
98     private int mKeyTextColor;
99     private int mRowCount;
100     private int mColCount;
101 
102     private class KeyHolder {
103         public boolean isInMiniKb = false;
104         public boolean isInvertible = false;
105         public Key key;
106 
KeyHolder(Key key)107         public KeyHolder(Key key) {
108             this.key = key;
109         }
110     }
111 
LeanbackKeyboardView(Context context, AttributeSet attrs)112     public LeanbackKeyboardView(Context context, AttributeSet attrs) {
113         super(context, attrs);
114 
115         final Resources res = context.getResources();
116         TypedArray a = context.getTheme()
117                 .obtainStyledAttributes(attrs, R.styleable.LeanbackKeyboardView, 0, 0);
118         mRowCount = a.getInteger(R.styleable.LeanbackKeyboardView_rowCount, -1);
119         mColCount = a.getInteger(R.styleable.LeanbackKeyboardView_columnCount, -1);
120 
121         mKeyTextSize = (int) res.getDimension(R.dimen.key_font_size);
122 
123         mPaint = new Paint();
124         mPaint.setAntiAlias(true);
125         mPaint.setTextSize(mKeyTextSize);
126         mPaint.setTextAlign(Align.CENTER);
127         mPaint.setAlpha(255);
128 
129         mPadding = new Rect(0, 0, 0, 0);
130 
131         mModeChangeTextSize = (int) res.getDimension(R.dimen.function_key_mode_change_font_size);
132 
133         mKeyTextColor = res.getColor(R.color.key_text_default);
134 
135         mFocusIndex = -1;
136 
137         mShiftState = SHIFT_OFF;
138 
139         mFocusedScale = res.getFraction(R.fraction.focused_scale, 1, 1);
140         mClickedScale = res.getFraction(R.fraction.clicked_scale, 1, 1);
141         mClickAnimDur = res.getInteger(R.integer.clicked_anim_duration);
142         mUnfocusStartDelay = res.getInteger(R.integer.unfocused_anim_delay);
143 
144         mInactiveMiniKbAlpha = res.getInteger(R.integer.inactive_mini_kb_alpha);
145     }
146 
147     /**
148      * Get the total rows of the keyboard
149      */
getRowCount()150     public int getRowCount() {
151         return mRowCount;
152     }
153 
154     /**
155      * Get the total columns of the keyboard
156      */
getColCount()157     public int getColCount() {
158         return mColCount;
159     }
160 
161     /**
162      * Get the key at the specified index
163      *
164      * @param index
165      * @return null if the keyboardView has not been assigned a keyboard
166      */
getKey(int index)167     public Key getKey(int index) {
168         if (mKeys == null || mKeys.length == 0 || index < 0 || index > mKeys.length) {
169             return null;
170         }
171         return mKeys[index].key;
172     }
173 
174     /**
175      * Get the current focused key
176      */
getFocusedKey()177     public Key getFocusedKey() {
178         return mFocusIndex == -1 ? null : mKeys[mFocusIndex].key;
179     }
180 
181     /**
182      * Get the keyboard that's attached to the keyboardView
183      */
getKeyboard()184     public Keyboard getKeyboard() {
185         return mKeyboard;
186     }
187 
188     /**
189      * Get the key that's the nearest to the given position
190      *
191      * @param x position in pixels
192      * @param y position in pixels
193      */
getNearestIndex(float x, float y)194     public int getNearestIndex(float x, float y) {
195         if (mKeys == null || mKeys.length == 0) {
196             return 0;
197         }
198         x -= getPaddingLeft();
199         y -= getPaddingTop();
200         float height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
201         float width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
202         int rows = getRowCount();
203         int cols = getColCount();
204         int row = (int) (y / height * rows);
205         if (row < 0) {
206             row = 0;
207         } else if (row >= rows) {
208             row = rows - 1;
209         }
210         int col = (int) (x / width * cols);
211         if (col < 0) {
212             col = 0;
213         } else if (col >= cols) {
214             col = cols - 1;
215         }
216         int index = mColCount * row + col;
217 
218         // at space key (space key is 7 keys wide)
219         if (index > 46 && index < 53) {
220             index = 46;
221         }
222 
223         // beyond space, remove 6 extra slots for space
224         if (index >= 53) {
225             index -= 6;
226         }
227 
228         if (index < 0) {
229             index = 0;
230         } else if (index >= mKeys.length) {
231             index = mKeys.length - 1;
232         }
233 
234         return index;
235     }
236 
237     /**
238      * Attaches a keyboard to this view. The keyboard can be switched at any
239      * time and the view will re-layout itself to accommodate the keyboard.
240      *
241      * @see Keyboard
242      * @see #getKeyboard()
243      * @param keyboard the keyboard to display in this view
244      */
setKeyboard(Keyboard keyboard)245     public void setKeyboard(Keyboard keyboard) {
246         // Remove any pending messages
247         removeMessages();
248         mKeyboard = keyboard;
249         setKeys(mKeyboard.getKeys());
250 
251         // reset shift state
252         int shiftState = mShiftState;
253         mShiftState = -1;
254         setShiftState(shiftState);
255 
256         requestLayout();
257         invalidateAllKeys();
258         // computeProximityThreshold(keyboard); // TODO
259     }
260 
createKeyImageView(int keyIndex)261     private ImageView createKeyImageView(int keyIndex) {
262 
263         final Rect padding = mPadding;
264         final int kbdPaddingLeft = getPaddingLeft();
265         final int kbdPaddingTop = getPaddingTop();
266         final KeyHolder keyHolder = mKeys[keyIndex];
267         final Key key = keyHolder.key;
268 
269         // Switch the character to uppercase if shift is pressed
270         adjustCase(keyHolder);
271         String label = key.label == null ? null : key.label.toString();
272         if (Log.isLoggable(TAG, Log.VERBOSE)) {
273             Log.d(TAG, "LABEL: " + key.label + "->" + label);
274         }
275 
276         Bitmap bitmap = Bitmap.createBitmap(key.width, key.height, Bitmap.Config.ARGB_8888);
277         Canvas canvas = new Canvas(bitmap);
278         final Paint paint = mPaint;
279         paint.setColor(mKeyTextColor);
280 
281         canvas.drawARGB(0, 0,  0,  0);
282 
283         if (key.icon != null) {
284             if (key.codes[0] == Keyboard.KEYCODE_SHIFT) {
285                 switch (mShiftState) {
286                     case SHIFT_OFF:
287                         key.icon = getContext().getResources().getDrawable(R.drawable.ic_ime_shift_off);
288                         break;
289                     case SHIFT_ON:
290                         key.icon = getContext().getResources().getDrawable(R.drawable.ic_ime_shift_on);
291                         break;
292                     case SHIFT_LOCKED:
293                         key.icon = getContext().getResources()
294                                 .getDrawable(R.drawable.ic_ime_shift_lock_on);
295                         break;
296                 }
297             }
298             final int drawableX = (key.width - padding.left - padding.right
299                     - key.icon.getIntrinsicWidth()) / 2 + padding.left;
300             final int drawableY = (key.height - padding.top - padding.bottom
301                     - key.icon.getIntrinsicHeight()) / 2 + padding.top;
302             canvas.translate(drawableX, drawableY);
303             key.icon.setBounds(0, 0,
304                     key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
305             key.icon.draw(canvas);
306             canvas.translate(-drawableX, -drawableY);
307         } else if (label != null) {
308             // For characters, use large font. For labels like "Done", use
309             // small font.
310             if (label.length() > 1) {
311                 paint.setTextSize(mModeChangeTextSize);
312                 paint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
313             } else {
314                 paint.setTextSize(mKeyTextSize);
315                 paint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
316             }
317             // Draw the text
318             canvas.drawText(label,
319                     (key.width - padding.left - padding.right) / 2
320                     + padding.left,
321                     (key.height - padding.top - padding.bottom) / 2
322                     + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
323                     paint);
324             // Turn off drop shadow
325             paint.setShadowLayer(0, 0, 0, 0);
326         }
327 
328         ImageView view = new ImageView(getContext());
329         view.setImageBitmap(bitmap);
330         view.setContentDescription(label);
331         addView(view, new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT,
332                 LayoutParams.WRAP_CONTENT));
333 
334         view.setX(key.x + kbdPaddingLeft);
335         view.setY(key.y + kbdPaddingTop);
336         view.setImageAlpha(mMiniKeyboardOnScreen && !keyHolder.isInMiniKb ?
337                 mInactiveMiniKbAlpha : 255);
338         view.setVisibility(View.VISIBLE);
339 
340         return view;
341     }
342 
createKeyImageViews(KeyHolder[] keys)343     private void createKeyImageViews(KeyHolder[] keys) {
344         int totalKeys = keys.length;
345         if (mKeyImageViews != null) {
346             for (ImageView view : mKeyImageViews) {
347                 this.removeView(view);
348             }
349             mKeyImageViews = null;
350         }
351 
352         for (int keyIndex = 0; keyIndex < totalKeys; keyIndex++) {
353             if (mKeyImageViews == null) {
354                 mKeyImageViews = new ImageView[totalKeys];
355             } else if (mKeyImageViews[keyIndex] != null) {
356                 removeView(mKeyImageViews[keyIndex]);
357             }
358             mKeyImageViews[keyIndex] = createKeyImageView(keyIndex);
359         }
360     }
361 
removeMessages()362     private void removeMessages() {
363         // TODO create mHandler and remove all messages here
364     }
365 
366     /**
367      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is
368      * not sufficient because the keyboard renders the keys to an off-screen
369      * buffer and an invalidate() only draws the cached buffer.
370      *
371      * @see #invalidateKey(int)
372      */
invalidateAllKeys()373     public void invalidateAllKeys() {
374         createKeyImageViews(mKeys);
375     }
376 
invalidateKey(int keyIndex)377     public void invalidateKey(int keyIndex) {
378         if (mKeys == null)
379             return;
380         if (keyIndex < 0 || keyIndex >= mKeys.length) {
381             return;
382         }
383         if (mKeyImageViews[keyIndex] != null) {
384             removeView(mKeyImageViews[keyIndex]);
385         }
386         mKeyImageViews[keyIndex] = createKeyImageView(keyIndex);
387     }
388 
389     @Override
onDraw(Canvas canvas)390     public void onDraw(Canvas canvas) {
391         super.onDraw(canvas);
392     }
393 
adjustCase(KeyHolder keyHolder)394     private CharSequence adjustCase(KeyHolder keyHolder) {
395         CharSequence label = keyHolder.key.label;
396 
397         if (label != null && label.length() < 3) {
398             // if we're adjusting the case of a basic letter in the mini keyboard,
399             // we want the opposite case
400             boolean invert = keyHolder.isInMiniKb && keyHolder.isInvertible;
401             if (mKeyboard.isShifted() ^ invert) {
402                 label = label.toString().toUpperCase();
403             } else {
404                 label = label.toString().toLowerCase();
405             }
406 
407             keyHolder.key.label = label;
408         }
409 
410         return label;
411     }
412 
setShiftState(int state)413     public void setShiftState(int state) {
414         if (mShiftState == state) {
415             return;
416         }
417         switch (state) {
418             case SHIFT_OFF:
419                 mKeyboard.setShifted(false);
420                 break;
421             case SHIFT_ON:
422             case SHIFT_LOCKED:
423                 mKeyboard.setShifted(true);
424                 break;
425         }
426         mShiftState = state;
427         invalidateAllKeys();
428     }
429 
getShiftState()430     public int getShiftState() {
431         return mShiftState;
432     }
433 
isShifted()434     public boolean isShifted() {
435         return mShiftState == SHIFT_ON || mShiftState == SHIFT_LOCKED;
436     }
437 
setFocus(int index, boolean clicked)438     public void setFocus(int index, boolean clicked) {
439         setFocus(index, clicked, true);
440     }
441 
setFocus(int index, boolean clicked, boolean showFocusScale)442     public void setFocus(int index, boolean clicked, boolean showFocusScale) {
443         if (mKeyImageViews == null || mKeyImageViews.length == 0) {
444             return;
445         }
446         if (index < 0 || index >= mKeyImageViews.length) {
447             index = NOT_A_KEY;
448         }
449 
450         if (index != mFocusIndex || clicked != mFocusClicked) {
451             if (index != mFocusIndex) {
452                 if (mFocusIndex != NOT_A_KEY) {
453                     LeanbackUtils.sendAccessibilityEvent(mKeyImageViews[mFocusIndex], false);
454                 }
455                 if (index != NOT_A_KEY) {
456                     LeanbackUtils.sendAccessibilityEvent(mKeyImageViews[index], true);
457                 }
458             }
459 
460             if (mCurrentFocusView != null) {
461                 mCurrentFocusView.animate().scaleX(1f).scaleY(1f)
462                         .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator)
463                         .setStartDelay(mUnfocusStartDelay);
464                 mCurrentFocusView.animate().setDuration(mClickAnimDur)
465                         .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator)
466                         .setStartDelay(mUnfocusStartDelay);
467             }
468             if (index != NOT_A_KEY) {
469                 float scale = clicked ? mClickedScale : (showFocusScale ? mFocusedScale : 1.0f);
470                 mCurrentFocusView = mKeyImageViews[index];
471                 mCurrentFocusView.animate().scaleX(scale).scaleY(scale)
472                         .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator)
473                         .setDuration(mClickAnimDur).start();
474             }
475             mFocusIndex = index;
476             mFocusClicked = clicked;
477 
478             // if focusing on a non-mini kb key, dismiss minikb
479             if (NOT_A_KEY != index && !mKeys[index].isInMiniKb) {
480                 dismissMiniKeyboard();
481             }
482         }
483     }
484 
isMiniKeyboardOnScreen()485     public boolean isMiniKeyboardOnScreen() {
486         return mMiniKeyboardOnScreen;
487     }
488 
onKeyLongPress()489     public void onKeyLongPress() {
490         int popupResId = mKeys[mFocusIndex].key.popupResId;
491         if (popupResId != 0) {
492             dismissMiniKeyboard();
493             mMiniKeyboardOnScreen = true;
494             Keyboard miniKeyboard = new Keyboard(getContext(), popupResId);
495             List<Key> accentKeys = miniKeyboard.getKeys();
496             int totalAccentKeys = accentKeys.size();
497             int baseIndex = mFocusIndex;
498             int currentRow = mFocusIndex / mColCount;
499             int nextRow = (mFocusIndex + totalAccentKeys) / mColCount;
500             // if all accent keys don't fit in a row when aligned with the popup
501             // key, align the accent keys to the right boundary of that row
502             if (currentRow != nextRow) {
503                 baseIndex = nextRow * mColCount - totalAccentKeys;
504             }
505             mBaseMiniKbIndex = baseIndex;
506             for (int i = 0; i < totalAccentKeys; i++) {
507                 Key accentKey = accentKeys.get(i);
508                 // inherit the key position and edge flags. this way the xml files for the each
509                 // miniKb don't have to take into account the configuration of the keyboard
510                 // they're being inserted into.
511                 accentKey.x = mKeys[baseIndex + i].key.x;
512                 accentKey.y = mKeys[baseIndex + i].key.y;
513                 accentKey.edgeFlags = mKeys[baseIndex + i].key.edgeFlags;
514                 mKeys[baseIndex + i].key = accentKey;
515                 mKeys[baseIndex + i].isInMiniKb = true;
516                 mKeys[baseIndex + i].isInvertible = (i == 0);
517             }
518 
519             invalidateAllKeys();
520         }
521     }
522 
getBaseMiniKbIndex()523     public int getBaseMiniKbIndex() {
524         return mBaseMiniKbIndex;
525     }
526 
527     /**
528      * @return  true if the minikeyboard was on-screen and is now dismissed, false otherwise.
529      */
dismissMiniKeyboard()530     public boolean dismissMiniKeyboard() {
531         if (mMiniKeyboardOnScreen) {
532             mMiniKeyboardOnScreen = false;
533             setKeys(mKeyboard.getKeys());
534             invalidateAllKeys();
535             return true;
536         }
537 
538         return false;
539     }
540 
setFocus(int row, int col, boolean clicked)541     public void setFocus(int row, int col, boolean clicked) {
542         setFocus(mColCount * row + col, clicked);
543     }
544 
545     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)546     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
547         // For the kids, ya know?
548         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
549         // Round up a little
550         if (mKeyboard == null) {
551             setMeasuredDimension(getPaddingLeft() + getPaddingRight(),
552                     getPaddingTop() + getPaddingBottom());
553         } else {
554             int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
555             if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
556                 width = MeasureSpec.getSize(widthMeasureSpec);
557             }
558             setMeasuredDimension(width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
559         }
560     }
561 
setKeys(List<Key> keys)562     private void setKeys(List<Key> keys) {
563         mKeys = new KeyHolder[keys.size()];
564         Iterator<Key> itt = keys.iterator();
565         for (int i = 0; i < mKeys.length && itt.hasNext(); i++) {
566             Key k = itt.next();
567             mKeys[i] = new KeyHolder(k);
568         }
569     }
570 }
571