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