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