Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2014 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.accessibility;
     18 
     19 import android.content.Context;
     20 import android.graphics.Rect;
     21 import android.os.SystemClock;
     22 import android.util.Log;
     23 import android.util.SparseIntArray;
     24 import android.view.MotionEvent;
     25 
     26 import com.android.inputmethod.keyboard.Key;
     27 import com.android.inputmethod.keyboard.KeyDetector;
     28 import com.android.inputmethod.keyboard.Keyboard;
     29 import com.android.inputmethod.keyboard.KeyboardId;
     30 import com.android.inputmethod.keyboard.MainKeyboardView;
     31 import com.android.inputmethod.keyboard.PointerTracker;
     32 import com.android.inputmethod.latin.R;
     33 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
     34 
     35 /**
     36  * This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance
     37  * accessibility support via composition rather via inheritance.
     38  */
     39 public final class MainKeyboardAccessibilityDelegate
     40         extends KeyboardAccessibilityDelegate<MainKeyboardView>
     41         implements AccessibilityLongPressTimer.LongPressTimerCallback {
     42     private static final String TAG = MainKeyboardAccessibilityDelegate.class.getSimpleName();
     43 
     44     /** Map of keyboard modes to resource IDs. */
     45     private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray();
     46 
     47     static {
     48         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date);
     49         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time);
     50         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email);
     51         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im);
     52         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number);
     53         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone);
     54         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text);
     55         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time);
     56         KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url);
     57     }
     58 
     59     /** The most recently set keyboard mode. */
     60     private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
     61     private static final int KEYBOARD_IS_HIDDEN = -1;
     62     // The rectangle region to ignore hover events.
     63     private final Rect mBoundsToIgnoreHoverEvent = new Rect();
     64 
     65     private final AccessibilityLongPressTimer mAccessibilityLongPressTimer;
     66 
     67     public MainKeyboardAccessibilityDelegate(final MainKeyboardView mainKeyboardView,
     68             final KeyDetector keyDetector) {
     69         super(mainKeyboardView, keyDetector);
     70         mAccessibilityLongPressTimer = new AccessibilityLongPressTimer(
     71                 this /* callback */, mainKeyboardView.getContext());
     72     }
     73 
     74     /**
     75      * {@inheritDoc}
     76      */
     77     @Override
     78     public void setKeyboard(final Keyboard keyboard) {
     79         if (keyboard == null) {
     80             return;
     81         }
     82         final Keyboard lastKeyboard = getKeyboard();
     83         super.setKeyboard(keyboard);
     84         final int lastKeyboardMode = mLastKeyboardMode;
     85         mLastKeyboardMode = keyboard.mId.mMode;
     86 
     87         // Since this method is called even when accessibility is off, make sure
     88         // to check the state before announcing anything.
     89         if (!AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
     90             return;
     91         }
     92         // Announce the language name only when the language is changed.
     93         if (lastKeyboard == null || !keyboard.mId.mSubtype.equals(lastKeyboard.mId.mSubtype)) {
     94             announceKeyboardLanguage(keyboard);
     95             return;
     96         }
     97         // Announce the mode only when the mode is changed.
     98         if (keyboard.mId.mMode != lastKeyboardMode) {
     99             announceKeyboardMode(keyboard);
    100             return;
    101         }
    102         // Announce the keyboard type only when the type is changed.
    103         if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) {
    104             announceKeyboardType(keyboard, lastKeyboard);
    105             return;
    106         }
    107     }
    108 
    109     /**
    110      * Called when the keyboard is hidden and accessibility is enabled.
    111      */
    112     public void onHideWindow() {
    113         announceKeyboardHidden();
    114         mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
    115     }
    116 
    117     /**
    118      * Announces which language of keyboard is being displayed.
    119      *
    120      * @param keyboard The new keyboard.
    121      */
    122     private void announceKeyboardLanguage(final Keyboard keyboard) {
    123         final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
    124                 keyboard.mId.mSubtype);
    125         sendWindowStateChanged(languageText);
    126     }
    127 
    128     /**
    129      * Announces which type of keyboard is being displayed.
    130      * If the keyboard type is unknown, no announcement is made.
    131      *
    132      * @param keyboard The new keyboard.
    133      */
    134     private void announceKeyboardMode(final Keyboard keyboard) {
    135         final Context context = mKeyboardView.getContext();
    136         final int modeTextResId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode);
    137         if (modeTextResId == 0) {
    138             return;
    139         }
    140         final String modeText = context.getString(modeTextResId);
    141         final String text = context.getString(R.string.announce_keyboard_mode, modeText);
    142         sendWindowStateChanged(text);
    143     }
    144 
    145     /**
    146      * Announces which type of keyboard is being displayed.
    147      *
    148      * @param keyboard The new keyboard.
    149      * @param lastKeyboard The last keyboard.
    150      */
    151     private void announceKeyboardType(final Keyboard keyboard, final Keyboard lastKeyboard) {
    152         final int lastElementId = lastKeyboard.mId.mElementId;
    153         final int resId;
    154         switch (keyboard.mId.mElementId) {
    155         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
    156         case KeyboardId.ELEMENT_ALPHABET:
    157             if (lastElementId == KeyboardId.ELEMENT_ALPHABET
    158                     || lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
    159                 // Transition between alphabet mode and automatic shifted mode should be silently
    160                 // ignored because it can be determined by each key's talk back announce.
    161                 return;
    162             }
    163             resId = R.string.spoken_description_mode_alpha;
    164             break;
    165         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
    166             if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
    167                 // Resetting automatic shifted mode by pressing the shift key causes the transition
    168                 // from automatic shifted to manual shifted that should be silently ignored.
    169                 return;
    170             }
    171             resId = R.string.spoken_description_shiftmode_on;
    172             break;
    173         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
    174             if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) {
    175                 // Resetting caps locked mode by pressing the shift key causes the transition
    176                 // from shift locked to shift lock shifted that should be silently ignored.
    177                 return;
    178             }
    179             resId = R.string.spoken_description_shiftmode_locked;
    180             break;
    181         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
    182             resId = R.string.spoken_description_shiftmode_locked;
    183             break;
    184         case KeyboardId.ELEMENT_SYMBOLS:
    185             resId = R.string.spoken_description_mode_symbol;
    186             break;
    187         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
    188             resId = R.string.spoken_description_mode_symbol_shift;
    189             break;
    190         case KeyboardId.ELEMENT_PHONE:
    191             resId = R.string.spoken_description_mode_phone;
    192             break;
    193         case KeyboardId.ELEMENT_PHONE_SYMBOLS:
    194             resId = R.string.spoken_description_mode_phone_shift;
    195             break;
    196         default:
    197             return;
    198         }
    199         sendWindowStateChanged(resId);
    200     }
    201 
    202     /**
    203      * Announces that the keyboard has been hidden.
    204      */
    205     private void announceKeyboardHidden() {
    206         sendWindowStateChanged(R.string.announce_keyboard_hidden);
    207     }
    208 
    209     @Override
    210     public void performClickOn(final Key key) {
    211         final int x = key.getHitBox().centerX();
    212         final int y = key.getHitBox().centerY();
    213         if (DEBUG_HOVER) {
    214             Log.d(TAG, "performClickOn: key=" + key
    215                     + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
    216         }
    217         if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
    218             // This hover exit event points to the key that should be ignored.
    219             // Clear the ignoring region to handle further hover events.
    220             mBoundsToIgnoreHoverEvent.setEmpty();
    221             return;
    222         }
    223         super.performClickOn(key);
    224     }
    225 
    226     @Override
    227     protected void onHoverEnterTo(final Key key) {
    228         final int x = key.getHitBox().centerX();
    229         final int y = key.getHitBox().centerY();
    230         if (DEBUG_HOVER) {
    231             Log.d(TAG, "onHoverEnterTo: key=" + key
    232                     + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
    233         }
    234         mAccessibilityLongPressTimer.cancelLongPress();
    235         if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
    236             return;
    237         }
    238         // This hover enter event points to the key that isn't in the ignoring region.
    239         // Further hover events should be handled.
    240         mBoundsToIgnoreHoverEvent.setEmpty();
    241         super.onHoverEnterTo(key);
    242         if (key.isLongPressEnabled()) {
    243             mAccessibilityLongPressTimer.startLongPress(key);
    244         }
    245     }
    246 
    247     @Override
    248     protected void onHoverExitFrom(final Key key) {
    249         final int x = key.getHitBox().centerX();
    250         final int y = key.getHitBox().centerY();
    251         if (DEBUG_HOVER) {
    252             Log.d(TAG, "onHoverExitFrom: key=" + key
    253                     + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
    254         }
    255         mAccessibilityLongPressTimer.cancelLongPress();
    256         super.onHoverExitFrom(key);
    257     }
    258 
    259     @Override
    260     public void performLongClickOn(final Key key) {
    261         if (DEBUG_HOVER) {
    262             Log.d(TAG, "performLongClickOn: key=" + key);
    263         }
    264         final PointerTracker tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID);
    265         final long eventTime = SystemClock.uptimeMillis();
    266         final int x = key.getHitBox().centerX();
    267         final int y = key.getHitBox().centerY();
    268         final MotionEvent downEvent = MotionEvent.obtain(
    269                 eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */);
    270         // Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
    271         tracker.processMotionEvent(downEvent, mKeyDetector);
    272         // The above fake down event triggers an unnecessary long press timer that should be
    273         // canceled.
    274         tracker.cancelLongPressTimer();
    275         downEvent.recycle();
    276         // Invoke {@link MainKeyboardView#onLongPress(PointerTracker)} as if a long press timeout
    277         // has passed.
    278         mKeyboardView.onLongPress(tracker);
    279         // If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
    280         // or a key invokes IME switcher dialog, we should just ignore the next
    281         // {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
    282         // {@link PointerTracker} is in operation or not.
    283         if (tracker.isInOperation()) {
    284             // This long press shows a more keys keyboard and further hover events should be
    285             // handled.
    286             mBoundsToIgnoreHoverEvent.setEmpty();
    287             return;
    288         }
    289         // This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
    290         // We should ignore further hover events on this key.
    291         mBoundsToIgnoreHoverEvent.set(key.getHitBox());
    292         if (key.hasNoPanelAutoMoreKey()) {
    293             // This long press has registered a code point without showing a more keys keyboard.
    294             // We should talk back the code point if possible.
    295             final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode;
    296             final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint(
    297                     mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey);
    298             if (text != null) {
    299                 sendWindowStateChanged(text);
    300             }
    301         }
    302     }
    303 }
    304