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