LatinIME/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDe...

304 lines
13 KiB
Java

/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.inputmethod.accessibility;
import android.content.Context;
import android.graphics.Rect;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.MotionEvent;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.keyboard.PointerTracker;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
/**
* This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance
* accessibility support via composition rather via inheritance.
*/
public final class MainKeyboardAccessibilityDelegate
extends KeyboardAccessibilityDelegate<MainKeyboardView>
implements AccessibilityLongPressTimer.LongPressTimerCallback {
private static final String TAG = MainKeyboardAccessibilityDelegate.class.getSimpleName();
/** Map of keyboard modes to resource IDs. */
private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray();
static {
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);
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);
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);
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);
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url);
}
/** The most recently set keyboard mode. */
private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
private static final int KEYBOARD_IS_HIDDEN = -1;
// The rectangle region to ignore hover events.
private final Rect mBoundsToIgnoreHoverEvent = new Rect();
private final AccessibilityLongPressTimer mAccessibilityLongPressTimer;
public MainKeyboardAccessibilityDelegate(final MainKeyboardView mainKeyboardView,
final KeyDetector keyDetector) {
super(mainKeyboardView, keyDetector);
mAccessibilityLongPressTimer = new AccessibilityLongPressTimer(
this /* callback */, mainKeyboardView.getContext());
}
/**
* {@inheritDoc}
*/
@Override
public void setKeyboard(final Keyboard keyboard) {
if (keyboard == null) {
return;
}
final Keyboard lastKeyboard = getKeyboard();
super.setKeyboard(keyboard);
final int lastKeyboardMode = mLastKeyboardMode;
mLastKeyboardMode = keyboard.mId.mMode;
// Since this method is called even when accessibility is off, make sure
// to check the state before announcing anything.
if (!AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
return;
}
// Announce the language name only when the language is changed.
if (lastKeyboard == null || !keyboard.mId.mSubtype.equals(lastKeyboard.mId.mSubtype)) {
announceKeyboardLanguage(keyboard);
return;
}
// Announce the mode only when the mode is changed.
if (keyboard.mId.mMode != lastKeyboardMode) {
announceKeyboardMode(keyboard);
return;
}
// Announce the keyboard type only when the type is changed.
if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) {
announceKeyboardType(keyboard, lastKeyboard);
return;
}
}
/**
* Called when the keyboard is hidden and accessibility is enabled.
*/
public void onHideWindow() {
announceKeyboardHidden();
mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
}
/**
* Announces which language of keyboard is being displayed.
*
* @param keyboard The new keyboard.
*/
private void announceKeyboardLanguage(final Keyboard keyboard) {
final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
keyboard.mId.mSubtype);
sendWindowStateChanged(languageText);
}
/**
* Announces which type of keyboard is being displayed.
* If the keyboard type is unknown, no announcement is made.
*
* @param keyboard The new keyboard.
*/
private void announceKeyboardMode(final Keyboard keyboard) {
final Context context = mKeyboardView.getContext();
final int modeTextResId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode);
if (modeTextResId == 0) {
return;
}
final String modeText = context.getString(modeTextResId);
final String text = context.getString(R.string.announce_keyboard_mode, modeText);
sendWindowStateChanged(text);
}
/**
* Announces which type of keyboard is being displayed.
*
* @param keyboard The new keyboard.
* @param lastKeyboard The last keyboard.
*/
private void announceKeyboardType(final Keyboard keyboard, final Keyboard lastKeyboard) {
final int lastElementId = lastKeyboard.mId.mElementId;
final int resId;
switch (keyboard.mId.mElementId) {
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET:
if (lastElementId == KeyboardId.ELEMENT_ALPHABET
|| lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
// Transition between alphabet mode and automatic shifted mode should be silently
// ignored because it can be determined by each key's talk back announce.
return;
}
resId = R.string.spoken_description_mode_alpha;
break;
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
// Resetting automatic shifted mode by pressing the shift key causes the transition
// from automatic shifted to manual shifted that should be silently ignored.
return;
}
resId = R.string.spoken_description_shiftmode_on;
break;
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) {
// Resetting caps locked mode by pressing the shift key causes the transition
// from shift locked to shift lock shifted that should be silently ignored.
return;
}
resId = R.string.spoken_description_shiftmode_locked;
break;
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
resId = R.string.spoken_description_shiftmode_locked;
break;
case KeyboardId.ELEMENT_SYMBOLS:
resId = R.string.spoken_description_mode_symbol;
break;
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
resId = R.string.spoken_description_mode_symbol_shift;
break;
case KeyboardId.ELEMENT_PHONE:
resId = R.string.spoken_description_mode_phone;
break;
case KeyboardId.ELEMENT_PHONE_SYMBOLS:
resId = R.string.spoken_description_mode_phone_shift;
break;
default:
return;
}
sendWindowStateChanged(resId);
}
/**
* Announces that the keyboard has been hidden.
*/
private void announceKeyboardHidden() {
sendWindowStateChanged(R.string.announce_keyboard_hidden);
}
@Override
public void performClickOn(final Key key) {
final int x = key.getHitBox().centerX();
final int y = key.getHitBox().centerY();
if (DEBUG_HOVER) {
Log.d(TAG, "performClickOn: key=" + key
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
}
if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
// This hover exit event points to the key that should be ignored.
// Clear the ignoring region to handle further hover events.
mBoundsToIgnoreHoverEvent.setEmpty();
return;
}
super.performClickOn(key);
}
@Override
protected void onHoverEnterTo(final Key key) {
final int x = key.getHitBox().centerX();
final int y = key.getHitBox().centerY();
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnterTo: key=" + key
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
}
mAccessibilityLongPressTimer.cancelLongPress();
if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
return;
}
// This hover enter event points to the key that isn't in the ignoring region.
// Further hover events should be handled.
mBoundsToIgnoreHoverEvent.setEmpty();
super.onHoverEnterTo(key);
if (key.isLongPressEnabled()) {
mAccessibilityLongPressTimer.startLongPress(key);
}
}
@Override
protected void onHoverExitFrom(final Key key) {
final int x = key.getHitBox().centerX();
final int y = key.getHitBox().centerY();
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExitFrom: key=" + key
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
}
mAccessibilityLongPressTimer.cancelLongPress();
super.onHoverExitFrom(key);
}
@Override
public void performLongClickOn(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "performLongClickOn: key=" + key);
}
final PointerTracker tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID);
final long eventTime = SystemClock.uptimeMillis();
final int x = key.getHitBox().centerX();
final int y = key.getHitBox().centerY();
final MotionEvent downEvent = MotionEvent.obtain(
eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */);
// Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
tracker.processMotionEvent(downEvent, mKeyDetector);
// The above fake down event triggers an unnecessary long press timer that should be
// canceled.
tracker.cancelLongPressTimer();
downEvent.recycle();
// Invoke {@link MainKeyboardView#onLongPress(PointerTracker)} as if a long press timeout
// has passed.
mKeyboardView.onLongPress(tracker);
// If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
// or a key invokes IME switcher dialog, we should just ignore the next
// {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
// {@link PointerTracker} is in operation or not.
if (tracker.isInOperation()) {
// This long press shows a more keys keyboard and further hover events should be
// handled.
mBoundsToIgnoreHoverEvent.setEmpty();
return;
}
// This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
// We should ignore further hover events on this key.
mBoundsToIgnoreHoverEvent.set(key.getHitBox());
if (key.hasNoPanelAutoMoreKey()) {
// This long press has registered a code point without showing a more keys keyboard.
// We should talk back the code point if possible.
final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode;
final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint(
mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey);
if (text != null) {
sendWindowStateChanged(text);
}
}
}
}