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