• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         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      */
announceKeyboardLanguage(final Keyboard keyboard)122     private void announceKeyboardLanguage(final Keyboard keyboard) {
123         final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
124                 keyboard.mId.mSubtype.getRawSubtype());
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      */
announceKeyboardMode(final Keyboard keyboard)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      */
announceKeyboardType(final Keyboard keyboard, final Keyboard lastKeyboard)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      */
announceKeyboardHidden()205     private void announceKeyboardHidden() {
206         sendWindowStateChanged(R.string.announce_keyboard_hidden);
207     }
208 
209     @Override
performClickOn(final Key key)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
onHoverEnterTo(final Key key)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
onHoverExitFrom(final Key key)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
performLongClickOn(final Key key)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         downEvent.recycle();
273         // Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
274         tracker.onLongPressed();
275         // If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
276         // or a key invokes IME switcher dialog, we should just ignore the next
277         // {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
278         // {@link PointerTracker} is in operation or not.
279         if (tracker.isInOperation()) {
280             // This long press shows a more keys keyboard and further hover events should be
281             // handled.
282             mBoundsToIgnoreHoverEvent.setEmpty();
283             return;
284         }
285         // This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
286         // We should ignore further hover events on this key.
287         mBoundsToIgnoreHoverEvent.set(key.getHitBox());
288         if (key.hasNoPanelAutoMoreKey()) {
289             // This long press has registered a code point without showing a more keys keyboard.
290             // We should talk back the code point if possible.
291             final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode;
292             final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint(
293                     mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey);
294             if (text != null) {
295                 sendWindowStateChanged(text);
296             }
297         }
298     }
299 }
300