convert accessibility package to kotlin

This commit is contained in:
dslul 2020-01-21 18:16:01 +01:00
parent a059a0798b
commit ae424c7202
22 changed files with 1486 additions and 1799 deletions

View file

@ -1,67 +0,0 @@
/*
* 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 org.dslul.openboard.inputmethod.accessibility;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import org.dslul.openboard.inputmethod.keyboard.Key;
import org.dslul.openboard.inputmethod.latin.R;
// Handling long press timer to show a more keys keyboard.
final class AccessibilityLongPressTimer extends Handler {
public interface LongPressTimerCallback {
public void performLongClickOn(Key key);
}
private static final int MSG_LONG_PRESS = 1;
private final LongPressTimerCallback mCallback;
private final long mConfigAccessibilityLongPressTimeout;
public AccessibilityLongPressTimer(final LongPressTimerCallback callback,
final Context context) {
super();
mCallback = callback;
mConfigAccessibilityLongPressTimeout = context.getResources().getInteger(
R.integer.config_accessibility_long_press_key_timeout);
}
@Override
public void handleMessage(final Message msg) {
switch (msg.what) {
case MSG_LONG_PRESS:
cancelLongPress();
mCallback.performLongClickOn((Key)msg.obj);
return;
default:
super.handleMessage(msg);
return;
}
}
public void startLongPress(final Key key) {
cancelLongPress();
final Message longPressMessage = obtainMessage(MSG_LONG_PRESS, key);
sendMessageDelayed(longPressMessage, mConfigAccessibilityLongPressTimeout);
}
public void cancelLongPress() {
removeMessages(MSG_LONG_PRESS);
}
}

View file

@ -0,0 +1,49 @@
package org.dslul.openboard.inputmethod.accessibility
import android.content.Context
import android.os.Handler
import android.os.Message
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.latin.R
// Handling long press timer to show a more keys keyboard.
internal class AccessibilityLongPressTimer(private val mCallback: LongPressTimerCallback,
context: Context) : Handler() {
interface LongPressTimerCallback {
fun performLongClickOn(key: Key)
}
private val mConfigAccessibilityLongPressTimeout: Long
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_LONG_PRESS -> {
cancelLongPress()
mCallback.performLongClickOn(msg.obj as Key)
return
}
else -> {
super.handleMessage(msg)
return
}
}
}
fun startLongPress(key: Key?) {
cancelLongPress()
val longPressMessage = obtainMessage(MSG_LONG_PRESS, key)
sendMessageDelayed(longPressMessage, mConfigAccessibilityLongPressTimeout)
}
fun cancelLongPress() {
removeMessages(MSG_LONG_PRESS)
}
companion object {
private const val MSG_LONG_PRESS = 1
}
init {
mConfigAccessibilityLongPressTimeout = context.resources.getInteger(
R.integer.config_accessibility_long_press_key_timeout).toLong()
}
}

View file

@ -1,262 +0,0 @@
/*
* Copyright (C) 2011 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 org.dslul.openboard.inputmethod.accessibility;
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
import android.os.SystemClock;
import android.provider.Settings;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.EditorInfo;
import org.dslul.openboard.inputmethod.compat.SettingsSecureCompatUtils;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.SuggestedWords;
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils;
public final class AccessibilityUtils {
private static final String TAG = AccessibilityUtils.class.getSimpleName();
private static final String CLASS = AccessibilityUtils.class.getName();
private static final String PACKAGE =
AccessibilityUtils.class.getPackage().getName();
private static final AccessibilityUtils sInstance = new AccessibilityUtils();
private Context mContext;
private AccessibilityManager mAccessibilityManager;
private AudioManager mAudioManager;
/** The most recent auto-correction. */
private String mAutoCorrectionWord;
/** The most recent typed word for auto-correction. */
private String mTypedWord;
/*
* Setting this constant to {@code false} will disable all keyboard
* accessibility code, regardless of whether Accessibility is turned on in
* the system settings. It should ONLY be used in the event of an emergency.
*/
private static final boolean ENABLE_ACCESSIBILITY = true;
public static void init(final Context context) {
if (!ENABLE_ACCESSIBILITY) return;
// These only need to be initialized if the kill switch is off.
sInstance.initInternal(context);
}
public static AccessibilityUtils getInstance() {
return sInstance;
}
private AccessibilityUtils() {
// This class is not publicly instantiable.
}
private void initInternal(final Context context) {
mContext = context;
mAccessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
/**
* Returns {@code true} if accessibility is enabled. Currently, this means
* that the kill switch is off and system accessibility is turned on.
*
* @return {@code true} if accessibility is enabled.
*/
public boolean isAccessibilityEnabled() {
return ENABLE_ACCESSIBILITY && mAccessibilityManager.isEnabled();
}
/**
* Returns {@code true} if touch exploration is enabled. Currently, this
* means that the kill switch is off, the device supports touch exploration,
* and system accessibility is turned on.
*
* @return {@code true} if touch exploration is enabled.
*/
public boolean isTouchExplorationEnabled() {
return isAccessibilityEnabled() && mAccessibilityManager.isTouchExplorationEnabled();
}
/**
* Returns {@true} if the provided event is a touch exploration (e.g. hover)
* event. This is used to determine whether the event should be processed by
* the touch exploration code within the keyboard.
*
* @param event The event to check.
* @return {@true} is the event is a touch exploration event
*/
public static boolean isTouchExplorationEvent(final MotionEvent event) {
final int action = event.getAction();
return action == MotionEvent.ACTION_HOVER_ENTER
|| action == MotionEvent.ACTION_HOVER_EXIT
|| action == MotionEvent.ACTION_HOVER_MOVE;
}
/**
* Returns whether the device should obscure typed password characters.
* Typically this means speaking "dot" in place of non-control characters.
*
* @return {@code true} if the device should obscure password characters.
*/
@SuppressWarnings("deprecation")
public boolean shouldObscureInput(final EditorInfo editorInfo) {
if (editorInfo == null) return false;
// The user can optionally force speaking passwords.
if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) {
final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(),
SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
if (speakPassword) return false;
}
// Always speak if the user is listening through headphones.
if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) {
return false;
}
// Don't speak if the IME is connected to a password field.
return InputTypeUtils.isPasswordInputType(editorInfo.inputType);
}
/**
* Sets the current auto-correction word and typed word. These may be used
* to provide the user with a spoken description of what auto-correction
* will occur when a key is typed.
*
* @param suggestedWords the list of suggested auto-correction words
*/
public void setAutoCorrection(final SuggestedWords suggestedWords) {
if (suggestedWords.mWillAutoCorrect) {
mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
final SuggestedWords.SuggestedWordInfo typedWordInfo = suggestedWords.mTypedWordInfo;
if (null == typedWordInfo) {
mTypedWord = null;
} else {
mTypedWord = typedWordInfo.mWord;
}
} else {
mAutoCorrectionWord = null;
mTypedWord = null;
}
}
/**
* Obtains a description for an auto-correction key, taking into account the
* currently typed word and auto-correction.
*
* @param keyCodeDescription spoken description of the key that will insert
* an auto-correction
* @param shouldObscure whether the key should be obscured
* @return a description including a description of the auto-correction, if
* needed
*/
public String getAutoCorrectionDescription(
final String keyCodeDescription, final boolean shouldObscure) {
if (!TextUtils.isEmpty(mAutoCorrectionWord)) {
if (!TextUtils.equals(mAutoCorrectionWord, mTypedWord)) {
if (shouldObscure) {
// This should never happen, but just in case...
return mContext.getString(R.string.spoken_auto_correct_obscured,
keyCodeDescription);
}
return mContext.getString(R.string.spoken_auto_correct, keyCodeDescription,
mTypedWord, mAutoCorrectionWord);
}
}
return keyCodeDescription;
}
/**
* Sends the specified text to the {@link AccessibilityManager} to be
* spoken.
*
* @param view The source view.
* @param text The text to speak.
*/
public void announceForAccessibility(final View view, final CharSequence text) {
if (!mAccessibilityManager.isEnabled()) {
Log.e(TAG, "Attempted to speak when accessibility was disabled!");
return;
}
// The following is a hack to avoid using the heavy-weight TextToSpeech
// class. Instead, we're just forcing a fake AccessibilityEvent into
// the screen reader to make it speak.
final AccessibilityEvent event = AccessibilityEvent.obtain();
event.setPackageName(PACKAGE);
event.setClassName(CLASS);
event.setEventTime(SystemClock.uptimeMillis());
event.setEnabled(true);
event.getText().add(text);
// Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use
// announce events.
event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
final ViewParent viewParent = view.getParent();
if ((viewParent == null) || !(viewParent instanceof ViewGroup)) {
Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility");
return;
}
viewParent.requestSendAccessibilityEvent(view, event);
}
/**
* Handles speaking the "connect a headset to hear passwords" notification
* when connecting to a password field.
*
* @param view The source view.
* @param editorInfo The input connection's editor info attribute.
* @param restarting Whether the connection is being restarted.
*/
public void onStartInputViewInternal(final View view, final EditorInfo editorInfo,
final boolean restarting) {
if (shouldObscureInput(editorInfo)) {
final CharSequence text = mContext.getText(R.string.spoken_use_headphones);
announceForAccessibility(view, text);
}
}
/**
* Sends the specified {@link AccessibilityEvent} if accessibility is
* enabled. No operation if accessibility is disabled.
*
* @param event The event to send.
*/
public void requestSendAccessibilityEvent(final AccessibilityEvent event) {
if (mAccessibilityManager.isEnabled()) {
mAccessibilityManager.sendAccessibilityEvent(event);
}
}
}

View file

@ -0,0 +1,210 @@
package org.dslul.openboard.inputmethod.accessibility
import android.content.Context
import android.media.AudioManager
import android.os.SystemClock
import android.provider.Settings
import android.text.TextUtils
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import android.view.inputmethod.EditorInfo
import androidx.core.view.accessibility.AccessibilityEventCompat
import org.dslul.openboard.inputmethod.compat.SettingsSecureCompatUtils
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.SuggestedWords
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils
class AccessibilityUtils private constructor() {
private var mContext: Context? = null
private var mAccessibilityManager: AccessibilityManager? = null
private var mAudioManager: AudioManager? = null
/** The most recent auto-correction. */
private var mAutoCorrectionWord: String? = null
/** The most recent typed word for auto-correction. */
private var mTypedWord: String? = null
private fun initInternal(context: Context) {
mContext = context
mAccessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
}
/**
* Returns `true` if accessibility is enabled. Currently, this means
* that the kill switch is off and system accessibility is turned on.
*
* @return `true` if accessibility is enabled.
*/
val isAccessibilityEnabled: Boolean
get() = ENABLE_ACCESSIBILITY && mAccessibilityManager!!.isEnabled
/**
* Returns `true` if touch exploration is enabled. Currently, this
* means that the kill switch is off, the device supports touch exploration,
* and system accessibility is turned on.
*
* @return `true` if touch exploration is enabled.
*/
val isTouchExplorationEnabled: Boolean
get() = isAccessibilityEnabled && mAccessibilityManager!!.isTouchExplorationEnabled
/**
* Returns whether the device should obscure typed password characters.
* Typically this means speaking "dot" in place of non-control characters.
*
* @return `true` if the device should obscure password characters.
*/
fun shouldObscureInput(editorInfo: EditorInfo?): Boolean {
if (editorInfo == null) return false
// The user can optionally force speaking passwords.
if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) {
val speakPassword = Settings.Secure.getInt(mContext!!.contentResolver,
SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0
if (speakPassword) return false
}
// Always speak if the user is listening through headphones.
return if (mAudioManager!!.isWiredHeadsetOn || mAudioManager!!.isBluetoothA2dpOn) {
false
} else InputTypeUtils.isPasswordInputType(editorInfo.inputType)
// Don't speak if the IME is connected to a password field.
}
/**
* Sets the current auto-correction word and typed word. These may be used
* to provide the user with a spoken description of what auto-correction
* will occur when a key is typed.
*
* @param suggestedWords the list of suggested auto-correction words
*/
fun setAutoCorrection(suggestedWords: SuggestedWords) {
if (suggestedWords.mWillAutoCorrect) {
mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION)
val typedWordInfo = suggestedWords.mTypedWordInfo
mTypedWord = typedWordInfo?.mWord
} else {
mAutoCorrectionWord = null
mTypedWord = null
}
}
/**
* Obtains a description for an auto-correction key, taking into account the
* currently typed word and auto-correction.
*
* @param keyCodeDescription spoken description of the key that will insert
* an auto-correction
* @param shouldObscure whether the key should be obscured
* @return a description including a description of the auto-correction, if
* needed
*/
fun getAutoCorrectionDescription(
keyCodeDescription: String?, shouldObscure: Boolean): String? {
if (!TextUtils.isEmpty(mAutoCorrectionWord)) {
if (!TextUtils.equals(mAutoCorrectionWord, mTypedWord)) {
return if (shouldObscure) { // This should never happen, but just in case...
mContext!!.getString(R.string.spoken_auto_correct_obscured,
keyCodeDescription)
} else mContext!!.getString(R.string.spoken_auto_correct, keyCodeDescription,
mTypedWord, mAutoCorrectionWord)
}
}
return keyCodeDescription
}
/**
* Sends the specified text to the [AccessibilityManager] to be
* spoken.
*
* @param view The source view.
* @param text The text to speak.
*/
fun announceForAccessibility(view: View, text: CharSequence?) {
if (!mAccessibilityManager!!.isEnabled) {
Log.e(TAG, "Attempted to speak when accessibility was disabled!")
return
}
// The following is a hack to avoid using the heavy-weight TextToSpeech
// class. Instead, we're just forcing a fake AccessibilityEvent into
// the screen reader to make it speak.
val event = AccessibilityEvent.obtain()
event.packageName = PACKAGE
event.className = CLASS
event.eventTime = SystemClock.uptimeMillis()
event.isEnabled = true
event.text.add(text)
// Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use
// announce events.
event.eventType = AccessibilityEventCompat.TYPE_ANNOUNCEMENT
val viewParent = view.parent
if (viewParent == null || viewParent !is ViewGroup) {
Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility")
return
}
viewParent.requestSendAccessibilityEvent(view, event)
}
/**
* Handles speaking the "connect a headset to hear passwords" notification
* when connecting to a password field.
*
* @param view The source view.
* @param editorInfo The input connection's editor info attribute.
* @param restarting Whether the connection is being restarted.
*/
fun onStartInputViewInternal(view: View, editorInfo: EditorInfo?,
restarting: Boolean) {
if (shouldObscureInput(editorInfo)) {
val text = mContext!!.getText(R.string.spoken_use_headphones)
announceForAccessibility(view, text)
}
}
/**
* Sends the specified [AccessibilityEvent] if accessibility is
* enabled. No operation if accessibility is disabled.
*
* @param event The event to send.
*/
fun requestSendAccessibilityEvent(event: AccessibilityEvent?) {
if (mAccessibilityManager!!.isEnabled) {
mAccessibilityManager!!.sendAccessibilityEvent(event)
}
}
companion object {
private val TAG = AccessibilityUtils::class.java.simpleName
private val CLASS = AccessibilityUtils::class.java.name
private val PACKAGE = AccessibilityUtils::class.java.getPackage()!!.name
public val instance = AccessibilityUtils()
/*
* Setting this constant to {@code false} will disable all keyboard
* accessibility code, regardless of whether Accessibility is turned on in
* the system settings. It should ONLY be used in the event of an emergency.
*/
private const val ENABLE_ACCESSIBILITY = true
@JvmStatic
fun init(context: Context) {
if (!ENABLE_ACCESSIBILITY) return
// These only need to be initialized if the kill switch is off.
instance.initInternal(context)
}
/**
* Returns {@true} if the provided event is a touch exploration (e.g. hover)
* event. This is used to determine whether the event should be processed by
* the touch exploration code within the keyboard.
*
* @param event The event to check.
* @return {@true} is the event is a touch exploration event
*/
fun isTouchExplorationEvent(event: MotionEvent): Boolean {
val action = event.action
return action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_EXIT || action == MotionEvent.ACTION_HOVER_MOVE
}
}
}

View file

@ -1,365 +0,0 @@
/*
* Copyright (C) 2011 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 org.dslul.openboard.inputmethod.accessibility;
import android.content.Context;
import android.content.res.Resources;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.inputmethod.EditorInfo;
import org.dslul.openboard.inputmethod.keyboard.Key;
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
import java.util.Locale;
final class KeyCodeDescriptionMapper {
private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X";
private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X";
private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X";
private static final String SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon";
private static final String SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X";
// The resource ID of the string spoken for obscured keys
private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot;
private static final KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
public static KeyCodeDescriptionMapper getInstance() {
return sInstance;
}
// Sparse array of spoken description resource IDs indexed by key codes
private final SparseIntArray mKeyCodeMap = new SparseIntArray();
private KeyCodeDescriptionMapper() {
// Special non-character codes defined in Keyboard
mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space);
mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete);
mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return);
mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings);
mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift);
mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic);
mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab);
mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH,
R.string.spoken_description_language_switch);
mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next);
mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS,
R.string.spoken_description_action_previous);
mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji);
// Because the upper-case and lower-case mappings of the following letters is depending on
// the locale, the upper case descriptions should be defined here. The lower case
// descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
// U+0049: "I" LATIN CAPITAL LETTER I
// U+0069: "i" LATIN SMALL LETTER I
// U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE
// U+0131: "ı" LATIN SMALL LETTER DOTLESS I
mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049);
mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130);
}
/**
* Returns the localized description of the action performed by a specified
* key based on the current keyboard state.
*
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @param key The key from which to obtain a description.
* @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
* @return a character sequence describing the action performed by pressing the key
*/
public String getDescriptionForKey(final Context context, final Keyboard keyboard,
final Key key, final boolean shouldObscure) {
final int code = key.getCode();
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
if (description != null) {
return description;
}
}
if (code == Constants.CODE_SHIFT) {
return getDescriptionForShiftKey(context, keyboard);
}
if (code == Constants.CODE_ENTER) {
// The following function returns the correct description in all action and
// regular enter cases, taking care of all modes.
return getDescriptionForActionKey(context, keyboard, key);
}
if (code == Constants.CODE_OUTPUT_TEXT) {
final String outputText = key.getOutputText();
final String description = getSpokenEmoticonDescription(context, outputText);
return TextUtils.isEmpty(description) ? outputText : description;
}
// Just attempt to speak the description.
if (code != Constants.CODE_UNSPECIFIED) {
// If the key description should be obscured, now is the time to do it.
final boolean isDefinedNonCtrl = Character.isDefined(code)
&& !Character.isISOControl(code);
if (shouldObscure && isDefinedNonCtrl) {
return context.getString(OBSCURED_KEY_RES_ID);
}
final String description = getDescriptionForCodePoint(context, code);
if (description != null) {
return description;
}
if (!TextUtils.isEmpty(key.getLabel())) {
return key.getLabel();
}
return context.getString(R.string.spoken_description_unknown);
}
return null;
}
/**
* Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
* key or {@code null} if there is not a description provided for the
* current keyboard context.
*
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @return a character sequence describing the action performed by pressing the key
*/
private static String getDescriptionForSwitchAlphaSymbol(final Context context,
final Keyboard keyboard) {
final KeyboardId keyboardId = keyboard.mId;
final int elementId = keyboardId.mElementId;
final int resId;
switch (elementId) {
case KeyboardId.ELEMENT_ALPHABET:
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
resId = R.string.spoken_description_to_symbol;
break;
case KeyboardId.ELEMENT_SYMBOLS:
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
resId = R.string.spoken_description_to_alpha;
break;
case KeyboardId.ELEMENT_PHONE:
resId = R.string.spoken_description_to_symbol;
break;
case KeyboardId.ELEMENT_PHONE_SYMBOLS:
resId = R.string.spoken_description_to_numeric;
break;
default:
Log.e(TAG, "Missing description for keyboard element ID:" + elementId);
return null;
}
return context.getString(resId);
}
/**
* Returns a context-sensitive description of the "Shift" key.
*
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @return A context-sensitive description of the "Shift" key.
*/
private static String getDescriptionForShiftKey(final Context context,
final Keyboard keyboard) {
final KeyboardId keyboardId = keyboard.mId;
final int elementId = keyboardId.mElementId;
final int resId;
switch (elementId) {
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
resId = R.string.spoken_description_caps_lock;
break;
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
resId = R.string.spoken_description_shift_shifted;
break;
case KeyboardId.ELEMENT_SYMBOLS:
resId = R.string.spoken_description_symbols_shift;
break;
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
resId = R.string.spoken_description_symbols_shift_shifted;
break;
default:
resId = R.string.spoken_description_shift;
}
return context.getString(resId);
}
/**
* Returns a context-sensitive description of the "Enter" action key.
*
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @param key The key to describe.
* @return Returns a context-sensitive description of the "Enter" action key.
*/
private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard,
final Key key) {
final KeyboardId keyboardId = keyboard.mId;
final int actionId = keyboardId.imeAction();
final int resId;
// Always use the label, if available.
if (!TextUtils.isEmpty(key.getLabel())) {
return key.getLabel().trim();
}
// Otherwise, use the action ID.
switch (actionId) {
case EditorInfo.IME_ACTION_SEARCH:
resId = R.string.spoken_description_search;
break;
case EditorInfo.IME_ACTION_GO:
resId = R.string.label_go_key;
break;
case EditorInfo.IME_ACTION_SEND:
resId = R.string.label_send_key;
break;
case EditorInfo.IME_ACTION_NEXT:
resId = R.string.label_next_key;
break;
case EditorInfo.IME_ACTION_DONE:
resId = R.string.label_done_key;
break;
case EditorInfo.IME_ACTION_PREVIOUS:
resId = R.string.label_previous_key;
break;
default:
resId = R.string.spoken_description_return;
}
return context.getString(resId);
}
/**
* Returns a localized character sequence describing what will happen when
* the specified key is pressed based on its key code point.
*
* @param context The package's context.
* @param codePoint The code point from which to obtain a description.
* @return a character sequence describing the code point.
*/
public String getDescriptionForCodePoint(final Context context, final int codePoint) {
// If the key description should be obscured, now is the time to do it.
final int index = mKeyCodeMap.indexOfKey(codePoint);
if (index >= 0) {
return context.getString(mKeyCodeMap.valueAt(index));
}
final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint);
if (accentedLetter != null) {
return accentedLetter;
}
// Here, <code>code</code> may be a base (non-accented) letter.
final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint);
if (unsupportedSymbol != null) {
return unsupportedSymbol;
}
final String emojiDescription = getSpokenEmojiDescription(context, codePoint);
if (emojiDescription != null) {
return emojiDescription;
}
if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
return StringUtils.newSingleCodePointString(codePoint);
}
return null;
}
// TODO: Remove this method once TTS supports those accented letters' verbalization.
private String getSpokenAccentedLetterDescription(final Context context, final int code) {
final boolean isUpperCase = Character.isUpperCase(code);
final int baseCode = isUpperCase ? Character.toLowerCase(code) : code;
final int baseIndex = mKeyCodeMap.indexOfKey(baseCode);
final int resId = (baseIndex >= 0) ? mKeyCodeMap.valueAt(baseIndex)
: getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT);
if (resId == 0) {
return null;
}
final String spokenText = context.getString(resId);
return isUpperCase ? context.getString(R.string.spoken_description_upper_case, spokenText)
: spokenText;
}
// TODO: Remove this method once TTS supports those symbols' verbalization.
private String getSpokenSymbolDescription(final Context context, final int code) {
final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT);
if (resId == 0) {
return null;
}
final String spokenText = context.getString(resId);
if (!TextUtils.isEmpty(spokenText)) {
return spokenText;
}
// If a translated description is empty, fall back to unknown symbol description.
return context.getString(R.string.spoken_symbol_unknown);
}
// TODO: Remove this method once TTS supports emoji verbalization.
private String getSpokenEmojiDescription(final Context context, final int code) {
final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT);
if (resId == 0) {
return null;
}
final String spokenText = context.getString(resId);
if (!TextUtils.isEmpty(spokenText)) {
return spokenText;
}
// If a translated description is empty, fall back to unknown emoji description.
return context.getString(R.string.spoken_emoji_unknown);
}
private int getSpokenDescriptionId(final Context context, final int code,
final String resourceNameFormat) {
final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code);
final Resources resources = context.getResources();
// Note that the resource package name may differ from the context package name.
final String resourcePackageName = resources.getResourcePackageName(
R.string.spoken_description_unknown);
final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
if (resId != 0) {
mKeyCodeMap.append(code, resId);
}
return resId;
}
// TODO: Remove this method once TTS supports emoticon verbalization.
private static String getSpokenEmoticonDescription(final Context context,
final String outputText) {
final StringBuilder sb = new StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX);
final int textLength = outputText.length();
for (int index = 0; index < textLength; index = outputText.offsetByCodePoints(index, 1)) {
final int codePoint = outputText.codePointAt(index);
sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint));
}
final String resourceName = sb.toString();
final Resources resources = context.getResources();
// Note that the resource package name may differ from the context package name.
final String resourcePackageName = resources.getResourcePackageName(
R.string.spoken_description_unknown);
final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
return (resId == 0) ? null : resources.getString(resId);
}
}

View file

@ -0,0 +1,288 @@
package org.dslul.openboard.inputmethod.accessibility
import android.content.Context
import android.text.TextUtils
import android.util.Log
import android.util.SparseIntArray
import android.view.inputmethod.EditorInfo
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Keyboard
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.Constants
import org.dslul.openboard.inputmethod.latin.common.StringUtils
import java.util.*
internal class KeyCodeDescriptionMapper private constructor() {
// Sparse array of spoken description resource IDs indexed by key codes
private val mKeyCodeMap = SparseIntArray()
/**
* Returns the localized description of the action performed by a specified
* key based on the current keyboard state.
*
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @param key The key from which to obtain a description.
* @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
* @return a character sequence describing the action performed by pressing the key
*/
fun getDescriptionForKey(context: Context, keyboard: Keyboard?,
key: Key, shouldObscure: Boolean): String? {
val code = key.code
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
val description = getDescriptionForSwitchAlphaSymbol(context, keyboard)
if (description != null) {
return description
}
}
if (code == Constants.CODE_SHIFT) {
return getDescriptionForShiftKey(context, keyboard)
}
if (code == Constants.CODE_ENTER) { // The following function returns the correct description in all action and
// regular enter cases, taking care of all modes.
return getDescriptionForActionKey(context, keyboard, key)
}
if (code == Constants.CODE_OUTPUT_TEXT) {
val outputText = key.outputText
val description = getSpokenEmoticonDescription(context, outputText)
return if (TextUtils.isEmpty(description)) outputText else description
}
// Just attempt to speak the description.
if (code != Constants.CODE_UNSPECIFIED) { // If the key description should be obscured, now is the time to do it.
val isDefinedNonCtrl = (Character.isDefined(code)
&& !Character.isISOControl(code))
if (shouldObscure && isDefinedNonCtrl) {
return context.getString(OBSCURED_KEY_RES_ID)
}
val description = getDescriptionForCodePoint(context, code)
if (description != null) {
return description
}
return if (!TextUtils.isEmpty(key.label)) {
key.label
} else context.getString(R.string.spoken_description_unknown)
}
return null
}
/**
* Returns a localized character sequence describing what will happen when
* the specified key is pressed based on its key code point.
*
* @param context The package's context.
* @param codePoint The code point from which to obtain a description.
* @return a character sequence describing the code point.
*/
fun getDescriptionForCodePoint(context: Context, codePoint: Int): String? { // If the key description should be obscured, now is the time to do it.
val index = mKeyCodeMap.indexOfKey(codePoint)
if (index >= 0) {
return context.getString(mKeyCodeMap.valueAt(index))
}
val accentedLetter = getSpokenAccentedLetterDescription(context, codePoint)
if (accentedLetter != null) {
return accentedLetter
}
// Here, <code>code</code> may be a base (non-accented) letter.
val unsupportedSymbol = getSpokenSymbolDescription(context, codePoint)
if (unsupportedSymbol != null) {
return unsupportedSymbol
}
val emojiDescription = getSpokenEmojiDescription(context, codePoint)
if (emojiDescription != null) {
return emojiDescription
}
return if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
StringUtils.newSingleCodePointString(codePoint)
} else null
}
// TODO: Remove this method once TTS supports those accented letters' verbalization.
private fun getSpokenAccentedLetterDescription(context: Context, code: Int): String? {
val isUpperCase = Character.isUpperCase(code)
val baseCode = if (isUpperCase) Character.toLowerCase(code) else code
val baseIndex = mKeyCodeMap.indexOfKey(baseCode)
val resId = if (baseIndex >= 0) mKeyCodeMap.valueAt(baseIndex) else getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT)
if (resId == 0) {
return null
}
val spokenText = context.getString(resId)
return if (isUpperCase) context.getString(R.string.spoken_description_upper_case, spokenText) else spokenText
}
// TODO: Remove this method once TTS supports those symbols' verbalization.
private fun getSpokenSymbolDescription(context: Context, code: Int): String? {
val resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT)
if (resId == 0) {
return null
}
val spokenText = context.getString(resId)
return if (!TextUtils.isEmpty(spokenText)) {
spokenText
} else context.getString(R.string.spoken_symbol_unknown)
// If a translated description is empty, fall back to unknown symbol description.
}
// TODO: Remove this method once TTS supports emoji verbalization.
private fun getSpokenEmojiDescription(context: Context, code: Int): String? {
val resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT)
if (resId == 0) {
return null
}
val spokenText = context.getString(resId)
return if (!TextUtils.isEmpty(spokenText)) {
spokenText
} else context.getString(R.string.spoken_emoji_unknown)
// If a translated description is empty, fall back to unknown emoji description.
}
private fun getSpokenDescriptionId(context: Context, code: Int,
resourceNameFormat: String): Int {
val resourceName = String.format(Locale.ROOT, resourceNameFormat, code)
val resources = context.resources
// Note that the resource package name may differ from the context package name.
val resourcePackageName = resources.getResourcePackageName(
R.string.spoken_description_unknown)
val resId = resources.getIdentifier(resourceName, "string", resourcePackageName)
if (resId != 0) {
mKeyCodeMap.append(code, resId)
}
return resId
}
companion object {
private val TAG = KeyCodeDescriptionMapper::class.java.simpleName
private const val SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X"
private const val SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X"
private const val SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X"
private const val SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon"
private const val SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X"
// The resource ID of the string spoken for obscured keys
private const val OBSCURED_KEY_RES_ID = R.string.spoken_description_dot
val instance = KeyCodeDescriptionMapper()
/**
* Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
* key or `null` if there is not a description provided for the
* current keyboard context.
*
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @return a character sequence describing the action performed by pressing the key
*/
private fun getDescriptionForSwitchAlphaSymbol(context: Context,
keyboard: Keyboard?): String? {
val keyboardId = keyboard!!.mId
val elementId = keyboardId.mElementId
val resId: Int
resId = when (elementId) {
KeyboardId.ELEMENT_ALPHABET, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_to_symbol
KeyboardId.ELEMENT_SYMBOLS, KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_to_alpha
KeyboardId.ELEMENT_PHONE -> R.string.spoken_description_to_symbol
KeyboardId.ELEMENT_PHONE_SYMBOLS -> R.string.spoken_description_to_numeric
else -> {
Log.e(TAG, "Missing description for keyboard element ID:$elementId")
return null
}
}
return context.getString(resId)
}
/**
* Returns a context-sensitive description of the "Shift" key.
*
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @return A context-sensitive description of the "Shift" key.
*/
private fun getDescriptionForShiftKey(context: Context,
keyboard: Keyboard?): String {
val keyboardId = keyboard!!.mId
val elementId = keyboardId.mElementId
val resId: Int
resId = when (elementId) {
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_caps_lock
KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> R.string.spoken_description_shift_shifted
KeyboardId.ELEMENT_SYMBOLS -> R.string.spoken_description_symbols_shift
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_symbols_shift_shifted
else -> R.string.spoken_description_shift
}
return context.getString(resId)
}
/**
* Returns a context-sensitive description of the "Enter" action key.
*
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @param key The key to describe.
* @return Returns a context-sensitive description of the "Enter" action key.
*/
private fun getDescriptionForActionKey(context: Context, keyboard: Keyboard?,
key: Key): String {
val keyboardId = keyboard!!.mId
val actionId = keyboardId.imeAction()
val resId: Int
// Always use the label, if available.
if (!TextUtils.isEmpty(key.label)) {
return key.label!!.trim { it <= ' ' }
}
resId = when (actionId) {
EditorInfo.IME_ACTION_SEARCH -> R.string.spoken_description_search
EditorInfo.IME_ACTION_GO -> R.string.label_go_key
EditorInfo.IME_ACTION_SEND -> R.string.label_send_key
EditorInfo.IME_ACTION_NEXT -> R.string.label_next_key
EditorInfo.IME_ACTION_DONE -> R.string.label_done_key
EditorInfo.IME_ACTION_PREVIOUS -> R.string.label_previous_key
else -> R.string.spoken_description_return
}
return context.getString(resId)
}
// TODO: Remove this method once TTS supports emoticon verbalization.
private fun getSpokenEmoticonDescription(context: Context,
outputText: String?): String? {
val sb = StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX)
val textLength = outputText!!.length
var index = 0
while (index < textLength) {
val codePoint = outputText.codePointAt(index)
sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint))
index = outputText.offsetByCodePoints(index, 1)
}
val resourceName = sb.toString()
val resources = context.resources
// Note that the resource package name may differ from the context package name.
val resourcePackageName = resources.getResourcePackageName(
R.string.spoken_description_unknown)
val resId = resources.getIdentifier(resourceName, "string", resourcePackageName)
return if (resId == 0) null else resources.getString(resId)
}
}
init { // Special non-character codes defined in Keyboard
mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space)
mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete)
mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return)
mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings)
mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift)
mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic)
mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol)
mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab)
mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH,
R.string.spoken_description_language_switch)
mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next)
mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS,
R.string.spoken_description_action_previous)
mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji)
// Because the upper-case and lower-case mappings of the following letters is depending on
// the locale, the upper case descriptions should be defined here. The lower case
// descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
// U+0049: "I" LATIN CAPITAL LETTER I
// U+0069: "i" LATIN SMALL LETTER I
// U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE
// U+0131: "ı" LATIN SMALL LETTER DOTLESS I
mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049)
mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130)
}
}

View file

@ -1,327 +0,0 @@
/*
* Copyright (C) 2011 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 org.dslul.openboard.inputmethod.accessibility;
import android.content.Context;
import android.os.SystemClock;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import org.dslul.openboard.inputmethod.keyboard.Key;
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
import org.dslul.openboard.inputmethod.keyboard.KeyboardView;
/**
* This class represents a delegate that can be registered in a class that extends
* {@link KeyboardView} to enhance accessibility support via composition rather via inheritance.
*
* To implement accessibility mode, the target keyboard view has to:<p>
* - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view.
* - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}.
*
* @param <KV> The keyboard view class type.
*/
public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
extends AccessibilityDelegateCompat {
private static final String TAG = KeyboardAccessibilityDelegate.class.getSimpleName();
protected static final boolean DEBUG_HOVER = false;
protected final KV mKeyboardView;
protected final KeyDetector mKeyDetector;
private Keyboard mKeyboard;
private KeyboardAccessibilityNodeProvider<KV> mAccessibilityNodeProvider;
private Key mLastHoverKey;
public static final int HOVER_EVENT_POINTER_ID = 0;
public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) {
super();
mKeyboardView = keyboardView;
mKeyDetector = keyDetector;
// Ensure that the view has an accessibility delegate.
ViewCompat.setAccessibilityDelegate(keyboardView, this);
}
/**
* Called when the keyboard layout changes.
* <p>
* <b>Note:</b> This method will be called even if accessibility is not
* enabled.
* @param keyboard The keyboard that is being set to the wrapping view.
*/
public void setKeyboard(final Keyboard keyboard) {
if (keyboard == null) {
return;
}
if (mAccessibilityNodeProvider != null) {
mAccessibilityNodeProvider.setKeyboard(keyboard);
}
mKeyboard = keyboard;
}
protected final Keyboard getKeyboard() {
return mKeyboard;
}
protected final void setLastHoverKey(final Key key) {
mLastHoverKey = key;
}
protected final Key getLastHoverKey() {
return mLastHoverKey;
}
/**
* Sends a window state change event with the specified string resource id.
*
* @param resId The string resource id of the text to send with the event.
*/
protected void sendWindowStateChanged(final int resId) {
if (resId == 0) {
return;
}
final Context context = mKeyboardView.getContext();
sendWindowStateChanged(context.getString(resId));
}
/**
* Sends a window state change event with the specified text.
*
* @param text The text to send with the event.
*/
protected void sendWindowStateChanged(final String text) {
final AccessibilityEvent stateChange = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
mKeyboardView.onInitializeAccessibilityEvent(stateChange);
stateChange.getText().add(text);
stateChange.setContentDescription(null);
final ViewParent parent = mKeyboardView.getParent();
if (parent != null) {
parent.requestSendAccessibilityEvent(mKeyboardView, stateChange);
}
}
/**
* Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK
* version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
* node hierarchy provider.
*
* @param host The host view for the provider.
* @return The accessibility node provider for the current keyboard.
*/
@Override
public KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider(final View host) {
return getAccessibilityNodeProvider();
}
/**
* @return A lazily-instantiated node provider for this view delegate.
*/
protected KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider() {
// Instantiate the provide only when requested. Since the system
// will call this method multiple times it is a good practice to
// cache the provider instance.
if (mAccessibilityNodeProvider == null) {
mAccessibilityNodeProvider =
new KeyboardAccessibilityNodeProvider<>(mKeyboardView, this);
}
return mAccessibilityNodeProvider;
}
/**
* Get a key that a hover event is on.
*
* @param event The hover event.
* @return key The key that the <code>event</code> is on.
*/
protected final Key getHoverKeyOf(final MotionEvent event) {
final int actionIndex = event.getActionIndex();
final int x = (int)event.getX(actionIndex);
final int y = (int)event.getY(actionIndex);
return mKeyDetector.detectHitKey(x, y);
}
/**
* Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
*
* @param event The hover event.
* @return {@code true} if the event is handled.
*/
public boolean onHoverEvent(final MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_HOVER_ENTER:
onHoverEnter(event);
break;
case MotionEvent.ACTION_HOVER_MOVE:
onHoverMove(event);
break;
case MotionEvent.ACTION_HOVER_EXIT:
onHoverExit(event);
break;
default:
Log.w(getClass().getSimpleName(), "Unknown hover event: " + event);
break;
}
return true;
}
/**
* Process {@link MotionEvent#ACTION_HOVER_ENTER} event.
*
* @param event A hover enter event.
*/
protected void onHoverEnter(final MotionEvent event) {
final Key key = getHoverKeyOf(event);
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnter: key=" + key);
}
if (key != null) {
onHoverEnterTo(key);
}
setLastHoverKey(key);
}
/**
* Process {@link MotionEvent#ACTION_HOVER_MOVE} event.
*
* @param event A hover move event.
*/
protected void onHoverMove(final MotionEvent event) {
final Key lastKey = getLastHoverKey();
final Key key = getHoverKeyOf(event);
if (key != lastKey) {
if (lastKey != null) {
onHoverExitFrom(lastKey);
}
if (key != null) {
onHoverEnterTo(key);
}
}
if (key != null) {
onHoverMoveWithin(key);
}
setLastHoverKey(key);
}
/**
* Process {@link MotionEvent#ACTION_HOVER_EXIT} event.
*
* @param event A hover exit event.
*/
protected void onHoverExit(final MotionEvent event) {
final Key lastKey = getLastHoverKey();
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
}
if (lastKey != null) {
onHoverExitFrom(lastKey);
}
final Key key = getHoverKeyOf(event);
// Make sure we're not getting an EXIT event because the user slid
// off the keyboard area, then force a key press.
if (key != null) {
performClickOn(key);
onHoverExitFrom(key);
}
setLastHoverKey(null);
}
/**
* Perform click on a key.
*
* @param key A key to be registered.
*/
public void performClickOn(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "performClickOn: key=" + key);
}
simulateTouchEvent(MotionEvent.ACTION_DOWN, key);
simulateTouchEvent(MotionEvent.ACTION_UP, key);
}
/**
* Simulating a touch event by injecting a synthesized touch event into {@link KeyboardView}.
*
* @param touchAction The action of the synthesizing touch event.
* @param key The key that a synthesized touch event is on.
*/
private void simulateTouchEvent(final int touchAction, final Key key) {
final int x = key.getHitBox().centerX();
final int y = key.getHitBox().centerY();
final long eventTime = SystemClock.uptimeMillis();
final MotionEvent touchEvent = MotionEvent.obtain(
eventTime, eventTime, touchAction, x, y, 0 /* metaState */);
mKeyboardView.onTouchEvent(touchEvent);
touchEvent.recycle();
}
/**
* Handles a hover enter event on a key.
*
* @param key The currently hovered key.
*/
protected void onHoverEnterTo(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnterTo: key=" + key);
}
key.onPressed();
mKeyboardView.invalidateKey(key);
final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider();
provider.onHoverEnterTo(key);
provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
}
/**
* Handles a hover move event on a key.
*
* @param key The currently hovered key.
*/
protected void onHoverMoveWithin(final Key key) { }
/**
* Handles a hover exit event on a key.
*
* @param key The currently hovered key.
*/
protected void onHoverExitFrom(final Key key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExitFrom: key=" + key);
}
key.onReleased();
mKeyboardView.invalidateKey(key);
final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider();
provider.onHoverExitFrom(key);
}
/**
* Perform long click on a key.
*
* @param key A key to be long pressed on.
*/
public void performLongClickOn(final Key key) {
// A extended class should override this method to implement long press.
}
}

View file

@ -0,0 +1,272 @@
package org.dslul.openboard.inputmethod.accessibility
import android.os.SystemClock
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.accessibility.AccessibilityEvent
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.KeyDetector
import org.dslul.openboard.inputmethod.keyboard.Keyboard
import org.dslul.openboard.inputmethod.keyboard.KeyboardView
/**
* This class represents a delegate that can be registered in a class that extends
* [KeyboardView] to enhance accessibility support via composition rather via inheritance.
*
* To implement accessibility mode, the target keyboard view has to:
*
*
* - Call [.setKeyboard] when a new keyboard is set to the keyboard view.
* - Dispatch a hover event by calling [.onHoverEnter].
*
* @param <KV> The keyboard view class type.
</KV> */
open class KeyboardAccessibilityDelegate<KV : KeyboardView?>(protected val mKeyboardView: KV, protected val mKeyDetector: KeyDetector) : AccessibilityDelegateCompat() {
private var mKeyboard: Keyboard? = null
private var mAccessibilityNodeProvider: KeyboardAccessibilityNodeProvider<KV>? = null
private var mLastHoverKey: Key? = null
protected open var lastHoverKey: Key?
get() = mLastHoverKey
set(key) {
mLastHoverKey = key
}
/**
* Called when the keyboard layout changes.
*
*
* **Note:** This method will be called even if accessibility is not
* enabled.
* @param keyboard The keyboard that is being set to the wrapping view.
*/
open var keyboard: Keyboard?
get() = mKeyboard
set(keyboard) {
if (keyboard == null) {
return
}
mAccessibilityNodeProvider!!.setKeyboard(keyboard)
mKeyboard = keyboard
}
/**
* Sends a window state change event with the specified string resource id.
*
* @param resId The string resource id of the text to send with the event.
*/
protected fun sendWindowStateChanged(resId: Int) {
if (resId == 0) {
return
}
val context = mKeyboardView!!.context
sendWindowStateChanged(context.getString(resId))
}
/**
* Sends a window state change event with the specified text.
*
* @param text The text to send with the event.
*/
protected fun sendWindowStateChanged(text: String?) {
val stateChange = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
mKeyboardView!!.onInitializeAccessibilityEvent(stateChange)
stateChange.text.add(text)
stateChange.contentDescription = null
val parent = mKeyboardView.parent
parent?.requestSendAccessibilityEvent(mKeyboardView, stateChange)
}
/**
* Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK
* version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
* node hierarchy provider.
*
* @param host The host view for the provider.
* @return The accessibility node provider for the current keyboard.
*/
override fun getAccessibilityNodeProvider(host: View): KeyboardAccessibilityNodeProvider<KV> {
return accessibilityNodeProvider
}// Instantiate the provide only when requested. Since the system
// will call this method multiple times it is a good practice to
// cache the provider instance.
/**
* @return A lazily-instantiated node provider for this view delegate.
*/
protected val accessibilityNodeProvider: KeyboardAccessibilityNodeProvider<KV>
get() { // Instantiate the provide only when requested. Since the system
// will call this method multiple times it is a good practice to
// cache the provider instance.
return mAccessibilityNodeProvider ?: KeyboardAccessibilityNodeProvider(mKeyboardView, this)
}
/**
* Get a key that a hover event is on.
*
* @param event The hover event.
* @return key The key that the `event` is on.
*/
protected fun getHoverKeyOf(event: MotionEvent): Key {
val actionIndex = event.actionIndex
val x = event.getX(actionIndex).toInt()
val y = event.getY(actionIndex).toInt()
return mKeyDetector.detectHitKey(x, y)
}
/**
* Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
*
* @param event The hover event.
* @return `true` if the event is handled.
*/
fun onHoverEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_HOVER_ENTER -> onHoverEnter(event)
MotionEvent.ACTION_HOVER_MOVE -> onHoverMove(event)
MotionEvent.ACTION_HOVER_EXIT -> onHoverExit(event)
else -> Log.w(javaClass.simpleName, "Unknown hover event: $event")
}
return true
}
/**
* Process [MotionEvent.ACTION_HOVER_ENTER] event.
*
* @param event A hover enter event.
*/
protected open fun onHoverEnter(event: MotionEvent) {
val key = getHoverKeyOf(event)
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnter: key=$key")
}
key?.let { onHoverEnterTo(it) }
mLastHoverKey = key
}
/**
* Process [MotionEvent.ACTION_HOVER_MOVE] event.
*
* @param event A hover move event.
*/
protected open fun onHoverMove(event: MotionEvent) {
val lastKey = mLastHoverKey
val key = getHoverKeyOf(event)
if (key !== lastKey) {
lastKey?.let { onHoverExitFrom(it) }
key?.let { onHoverEnterTo(it) }
}
key?.let { onHoverMoveWithin(it) }
mLastHoverKey = key
}
/**
* Process [MotionEvent.ACTION_HOVER_EXIT] event.
*
* @param event A hover exit event.
*/
protected open fun onHoverExit(event: MotionEvent) {
val lastKey = mLastHoverKey
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey)
}
lastKey?.let { onHoverExitFrom(it) }
val key = getHoverKeyOf(event)
// Make sure we're not getting an EXIT event because the user slid
// off the keyboard area, then force a key press.
performClickOn(key)
onHoverExitFrom(key)
mLastHoverKey = null
}
/**
* Perform click on a key.
*
* @param key A key to be registered.
*/
open fun performClickOn(key: Key) {
if (DEBUG_HOVER) {
Log.d(TAG, "performClickOn: key=$key")
}
simulateTouchEvent(MotionEvent.ACTION_DOWN, key)
simulateTouchEvent(MotionEvent.ACTION_UP, key)
}
/**
* Simulating a touch event by injecting a synthesized touch event into [KeyboardView].
*
* @param touchAction The action of the synthesizing touch event.
* @param key The key that a synthesized touch event is on.
*/
private fun simulateTouchEvent(touchAction: Int, key: Key) {
val x = key.hitBox.centerX()
val y = key.hitBox.centerY()
val eventTime = SystemClock.uptimeMillis()
val touchEvent = MotionEvent.obtain(
eventTime, eventTime, touchAction, x.toFloat(), y.toFloat(), 0 /* metaState */)
mKeyboardView!!.onTouchEvent(touchEvent)
touchEvent.recycle()
}
/**
* Handles a hover enter event on a key.
*
* @param key The currently hovered key.
*/
protected open fun onHoverEnterTo(key: Key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnterTo: key=$key")
}
key.onPressed()
mKeyboardView!!.invalidateKey(key)
val provider = accessibilityNodeProvider
provider.onHoverEnterTo(key)
provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS)
}
/**
* Handles a hover move event on a key.
*
* @param key The currently hovered key.
*/
protected fun onHoverMoveWithin(key: Key?) {}
/**
* Handles a hover exit event on a key.
*
* @param key The currently hovered key.
*/
protected open fun onHoverExitFrom(key: Key) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExitFrom: key=$key")
}
key.onReleased()
mKeyboardView!!.invalidateKey(key)
val provider = accessibilityNodeProvider
provider.onHoverExitFrom(key)
}
/**
* Perform long click on a key.
*
* @param key A key to be long pressed on.
*/
open fun performLongClickOn(key: Key) { // A extended class should override this method to implement long press.
}
companion object {
private val TAG = KeyboardAccessibilityDelegate::class.java.simpleName
const val DEBUG_HOVER = false
const val HOVER_EVENT_POINTER_ID = 0
}
init {
// Ensure that the view has an accessibility delegate.
ViewCompat.setAccessibilityDelegate(mKeyboardView!!, this)
}
}

View file

@ -1,342 +0,0 @@
/*
* Copyright (C) 2012 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 org.dslul.openboard.inputmethod.accessibility;
import android.graphics.Rect;
import android.os.Bundle;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
import androidx.core.view.accessibility.AccessibilityRecordCompat;
import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;
import org.dslul.openboard.inputmethod.keyboard.Key;
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
import org.dslul.openboard.inputmethod.keyboard.KeyboardView;
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
import java.util.List;
/**
* Exposes a virtual view sub-tree for {@link KeyboardView} and generates
* {@link AccessibilityEvent}s for individual {@link Key}s.
* <p>
* A virtual sub-tree is composed of imaginary {@link View}s that are reported
* as a part of the view hierarchy for accessibility purposes. This enables
* custom views that draw complex content to report them selves as a tree of
* virtual views, thus conveying their logical structure.
* </p>
*/
final class KeyboardAccessibilityNodeProvider<KV extends KeyboardView>
extends AccessibilityNodeProviderCompat {
private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName();
// From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}.
private static final int UNDEFINED = Integer.MAX_VALUE;
private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
private final AccessibilityUtils mAccessibilityUtils;
/** Temporary rect used to calculate in-screen bounds. */
private final Rect mTempBoundsInScreen = new Rect();
/** The parent view's cached on-screen location. */
private final int[] mParentLocation = CoordinateUtils.newInstance();
/** The virtual view identifier for the focused node. */
private int mAccessibilityFocusedView = UNDEFINED;
/** The virtual view identifier for the hovering node. */
private int mHoveringNodeId = UNDEFINED;
/** The keyboard view to provide an accessibility node info. */
private final KV mKeyboardView;
/** The accessibility delegate. */
private final KeyboardAccessibilityDelegate<KV> mDelegate;
/** The current keyboard. */
private Keyboard mKeyboard;
public KeyboardAccessibilityNodeProvider(final KV keyboardView,
final KeyboardAccessibilityDelegate<KV> delegate) {
super();
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
mAccessibilityUtils = AccessibilityUtils.getInstance();
mKeyboardView = keyboardView;
mDelegate = delegate;
// Since this class is constructed lazily, we might not get a subsequent
// call to setKeyboard() and therefore need to call it now.
setKeyboard(keyboardView.getKeyboard());
}
/**
* Sets the keyboard represented by this node provider.
*
* @param keyboard The keyboard that is being set to the keyboard view.
*/
public void setKeyboard(final Keyboard keyboard) {
mKeyboard = keyboard;
}
private Key getKeyOf(final int virtualViewId) {
if (mKeyboard == null) {
return null;
}
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
// Use a virtual view id as an index of the sorted keys list.
if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) {
return sortedKeys.get(virtualViewId);
}
return null;
}
private int getVirtualViewIdOf(final Key key) {
if (mKeyboard == null) {
return View.NO_ID;
}
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
final int size = sortedKeys.size();
for (int index = 0; index < size; index++) {
if (sortedKeys.get(index) == key) {
// Use an index of the sorted keys list as a virtual view id.
return index;
}
}
return View.NO_ID;
}
/**
* Creates and populates an {@link AccessibilityEvent} for the specified key
* and event type.
*
* @param key A key on the host keyboard view.
* @param eventType The event type to create.
* @return A populated {@link AccessibilityEvent} for the key.
* @see AccessibilityEvent
*/
public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) {
final int virtualViewId = getVirtualViewIdOf(key);
final String keyDescription = getKeyDescription(key);
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.setPackageName(mKeyboardView.getContext().getPackageName());
event.setClassName(key.getClass().getName());
event.setContentDescription(keyDescription);
event.setEnabled(true);
final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
record.setSource(mKeyboardView, virtualViewId);
return event;
}
public void onHoverEnterTo(final Key key) {
final int id = getVirtualViewIdOf(key);
if (id == View.NO_ID) {
return;
}
// Start hovering on the key. Because our accessibility model is lift-to-type, we should
// report the node info without click and long click actions to avoid unnecessary
// announcements.
mHoveringNodeId = id;
// Invalidate the node info of the key.
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
}
public void onHoverExitFrom(final Key key) {
mHoveringNodeId = UNDEFINED;
// Invalidate the node info of the key to be able to revert the change we have done
// in {@link #onHoverEnterTo(Key)}.
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
}
/**
* Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
* view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
* the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
* <p>
* A virtual descendant is an imaginary View that is reported as a part of
* the view hierarchy for accessibility purposes. This enables custom views
* that draw complex content to report them selves as a tree of virtual
* views, thus conveying their logical structure.
* </p>
* <p>
* The implementer is responsible for obtaining an accessibility node info
* from the pool of reusable instances and setting the desired properties of
* the node info before returning it.
* </p>
*
* @param virtualViewId A client defined virtual view id.
* @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host
* View.
* @see AccessibilityNodeInfoCompat
*/
@Override
public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) {
if (virtualViewId == UNDEFINED) {
return null;
}
if (virtualViewId == View.NO_ID) {
// We are requested to create an AccessibilityNodeInfo describing
// this View, i.e. the root of the virtual sub-tree.
final AccessibilityNodeInfoCompat rootInfo =
AccessibilityNodeInfoCompat.obtain(mKeyboardView);
ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo);
updateParentLocation();
// Add the virtual children of the root View.
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
final int size = sortedKeys.size();
for (int index = 0; index < size; index++) {
final Key key = sortedKeys.get(index);
if (key.isSpacer()) {
continue;
}
// Use an index of the sorted keys list as a virtual view id.
rootInfo.addChild(mKeyboardView, index);
}
return rootInfo;
}
// Find the key that corresponds to the given virtual view id.
final Key key = getKeyOf(virtualViewId);
if (key == null) {
Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
return null;
}
final String keyDescription = getKeyDescription(key);
final Rect boundsInParent = key.getHitBox();
// Calculate the key's in-screen bounds.
mTempBoundsInScreen.set(boundsInParent);
mTempBoundsInScreen.offset(
CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation));
final Rect boundsInScreen = mTempBoundsInScreen;
// Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
info.setPackageName(mKeyboardView.getContext().getPackageName());
info.setClassName(key.getClass().getName());
info.setContentDescription(keyDescription);
info.setBoundsInParent(boundsInParent);
info.setBoundsInScreen(boundsInScreen);
info.setParent(mKeyboardView);
info.setSource(mKeyboardView, virtualViewId);
info.setEnabled(key.isEnabled());
info.setVisibleToUser(true);
// Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key.
// See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}.
if (virtualViewId != mHoveringNodeId) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
if (key.isLongPressEnabled()) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
}
}
if (mAccessibilityFocusedView == virtualViewId) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
} else {
info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
}
return info;
}
@Override
public boolean performAction(final int virtualViewId, final int action,
final Bundle arguments) {
final Key key = getKeyOf(virtualViewId);
if (key == null) {
return false;
}
return performActionForKey(key, action);
}
/**
* Performs the specified accessibility action for the given key.
*
* @param key The on which to perform the action.
* @param action The action to perform.
* @return The result of performing the action, or false if the action is not supported.
*/
boolean performActionForKey(final Key key, final int action) {
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
mAccessibilityFocusedView = getVirtualViewIdOf(key);
sendAccessibilityEventForKey(
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
return true;
case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
mAccessibilityFocusedView = UNDEFINED;
sendAccessibilityEventForKey(
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
return true;
case AccessibilityNodeInfoCompat.ACTION_CLICK:
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED);
mDelegate.performClickOn(key);
return true;
case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
mDelegate.performLongClickOn(key);
return true;
default:
return false;
}
}
/**
* Sends an accessibility event for the given {@link Key}.
*
* @param key The key that's sending the event.
* @param eventType The type of event to send.
*/
void sendAccessibilityEventForKey(final Key key, final int eventType) {
final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
mAccessibilityUtils.requestSendAccessibilityEvent(event);
}
/**
* Returns the context-specific description for a {@link Key}.
*
* @param key The key to describe.
* @return The context-specific description of the key.
*/
private String getKeyDescription(final Key key) {
final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo;
final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
final SettingsValues currentSettings = Settings.getInstance().getCurrent();
final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
mKeyboardView.getContext(), mKeyboard, key, shouldObscure);
if (currentSettings.isWordSeparator(key.getCode())) {
return mAccessibilityUtils.getAutoCorrectionDescription(
keyCodeDescription, shouldObscure);
}
return keyCodeDescription;
}
/**
* Updates the parent's on-screen location.
*/
private void updateParentLocation() {
mKeyboardView.getLocationOnScreen(mParentLocation);
}
}

View file

@ -0,0 +1,303 @@
package org.dslul.openboard.inputmethod.accessibility
import android.graphics.Rect
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.accessibility.AccessibilityEvent
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityEventCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
import org.dslul.openboard.inputmethod.keyboard.Key
import org.dslul.openboard.inputmethod.keyboard.Keyboard
import org.dslul.openboard.inputmethod.keyboard.KeyboardView
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils
import org.dslul.openboard.inputmethod.latin.settings.Settings
/**
* Exposes a virtual view sub-tree for [KeyboardView] and generates
* [AccessibilityEvent]s for individual [Key]s.
*
*
* A virtual sub-tree is composed of imaginary [View]s that are reported
* as a part of the view hierarchy for accessibility purposes. This enables
* custom views that draw complex content to report them selves as a tree of
* virtual views, thus conveying their logical structure.
*
*/
class KeyboardAccessibilityNodeProvider<KV : KeyboardView?>(keyboardView: KV,
delegate: KeyboardAccessibilityDelegate<KV>) : AccessibilityNodeProviderCompat() {
private val mKeyCodeDescriptionMapper: KeyCodeDescriptionMapper
private val mAccessibilityUtils: AccessibilityUtils
/** Temporary rect used to calculate in-screen bounds. */
private val mTempBoundsInScreen = Rect()
/** The parent view's cached on-screen location. */
private val mParentLocation = CoordinateUtils.newInstance()
/** The virtual view identifier for the focused node. */
private var mAccessibilityFocusedView = UNDEFINED
/** The virtual view identifier for the hovering node. */
private var mHoveringNodeId = UNDEFINED
/** The keyboard view to provide an accessibility node info. */
private val mKeyboardView: KV
/** The accessibility delegate. */
private val mDelegate: KeyboardAccessibilityDelegate<KV>
/** The current keyboard. */
private var mKeyboard: Keyboard? = null
/**
* Sets the keyboard represented by this node provider.
*
* @param keyboard The keyboard that is being set to the keyboard view.
*/
fun setKeyboard(keyboard: Keyboard?) {
mKeyboard = keyboard
}
private fun getKeyOf(virtualViewId: Int): Key? {
if (mKeyboard == null) {
return null
}
val sortedKeys = mKeyboard!!.sortedKeys
// Use a virtual view id as an index of the sorted keys list.
return if (virtualViewId >= 0 && virtualViewId < sortedKeys.size) {
sortedKeys[virtualViewId]
} else null
}
private fun getVirtualViewIdOf(key: Key): Int {
if (mKeyboard == null) {
return View.NO_ID
}
val sortedKeys = mKeyboard!!.sortedKeys
val size = sortedKeys.size
for (index in 0 until size) {
if (sortedKeys[index] === key) { // Use an index of the sorted keys list as a virtual view id.
return index
}
}
return View.NO_ID
}
/**
* Creates and populates an [AccessibilityEvent] for the specified key
* and event type.
*
* @param key A key on the host keyboard view.
* @param eventType The event type to create.
* @return A populated [AccessibilityEvent] for the key.
* @see AccessibilityEvent
*/
fun createAccessibilityEvent(key: Key, eventType: Int): AccessibilityEvent {
val virtualViewId = getVirtualViewIdOf(key)
val keyDescription = getKeyDescription(key)
val event = AccessibilityEvent.obtain(eventType)
event.packageName = mKeyboardView!!.context.packageName
event.className = key.javaClass.name
event.contentDescription = keyDescription
event.isEnabled = true
val record = AccessibilityEventCompat.asRecord(event)
record.setSource(mKeyboardView, virtualViewId)
return event
}
fun onHoverEnterTo(key: Key) {
val id = getVirtualViewIdOf(key)
if (id == View.NO_ID) {
return
}
// Start hovering on the key. Because our accessibility model is lift-to-type, we should
// report the node info without click and long click actions to avoid unnecessary
// announcements.
mHoveringNodeId = id
// Invalidate the node info of the key.
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER)
}
fun onHoverExitFrom(key: Key) {
mHoveringNodeId = UNDEFINED
// Invalidate the node info of the key to be able to revert the change we have done
// in {@link #onHoverEnterTo(Key)}.
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT)
}
/**
* Returns an [AccessibilityNodeInfoCompat] representing a virtual
* view, i.e. a descendant of the host View, with the given `virtualViewId` or
* the host View itself if `virtualViewId` equals to [View.NO_ID].
*
*
* A virtual descendant is an imaginary View that is reported as a part of
* the view hierarchy for accessibility purposes. This enables custom views
* that draw complex content to report them selves as a tree of virtual
* views, thus conveying their logical structure.
*
*
*
* The implementer is responsible for obtaining an accessibility node info
* from the pool of reusable instances and setting the desired properties of
* the node info before returning it.
*
*
* @param virtualViewId A client defined virtual view id.
* @return A populated [AccessibilityNodeInfoCompat] for a virtual descendant or the host
* View.
* @see AccessibilityNodeInfoCompat
*/
override fun createAccessibilityNodeInfo(virtualViewId: Int): AccessibilityNodeInfoCompat? {
if (virtualViewId == UNDEFINED) {
return null
}
if (virtualViewId == View.NO_ID) { // We are requested to create an AccessibilityNodeInfo describing
// this View, i.e. the root of the virtual sub-tree.
val rootInfo = AccessibilityNodeInfoCompat.obtain(mKeyboardView)
ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView!!, rootInfo)
updateParentLocation()
// Add the virtual children of the root View.
val sortedKeys = mKeyboard!!.sortedKeys
val size = sortedKeys.size
for (index in 0 until size) {
val key = sortedKeys[index]
if (key.isSpacer) {
continue
}
// Use an index of the sorted keys list as a virtual view id.
rootInfo.addChild(mKeyboardView, index)
}
return rootInfo
}
// Find the key that corresponds to the given virtual view id.
val key = getKeyOf(virtualViewId)
if (key == null) {
Log.e(TAG, "Invalid virtual view ID: $virtualViewId")
return null
}
val keyDescription = getKeyDescription(key)
val boundsInParent = key.hitBox
// Calculate the key's in-screen bounds.
mTempBoundsInScreen.set(boundsInParent)
mTempBoundsInScreen.offset(
CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation))
val boundsInScreen = mTempBoundsInScreen
// Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
val info = AccessibilityNodeInfoCompat.obtain()
info.packageName = mKeyboardView!!.context.packageName
info.className = key.javaClass.name
info.contentDescription = keyDescription
info.setBoundsInParent(boundsInParent)
info.setBoundsInScreen(boundsInScreen)
info.setParent(mKeyboardView)
info.setSource(mKeyboardView, virtualViewId)
info.isEnabled = key.isEnabled
info.isVisibleToUser = true
// Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key.
// See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}.
if (virtualViewId != mHoveringNodeId) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
if (key.isLongPressEnabled) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK)
}
}
if (mAccessibilityFocusedView == virtualViewId) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS)
} else {
info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS)
}
return info
}
override fun performAction(virtualViewId: Int, action: Int,
arguments: Bundle): Boolean {
val key = getKeyOf(virtualViewId) ?: return false
return performActionForKey(key, action)
}
/**
* Performs the specified accessibility action for the given key.
*
* @param key The on which to perform the action.
* @param action The action to perform.
* @return The result of performing the action, or false if the action is not supported.
*/
fun performActionForKey(key: Key, action: Int): Boolean {
return when (action) {
AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS -> {
mAccessibilityFocusedView = getVirtualViewIdOf(key)
sendAccessibilityEventForKey(
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
true
}
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS -> {
mAccessibilityFocusedView = UNDEFINED
sendAccessibilityEventForKey(
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)
true
}
AccessibilityNodeInfoCompat.ACTION_CLICK -> {
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED)
mDelegate.performClickOn(key)
true
}
AccessibilityNodeInfoCompat.ACTION_LONG_CLICK -> {
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED)
mDelegate.performLongClickOn(key)
true
}
else -> false
}
}
/**
* Sends an accessibility event for the given [Key].
*
* @param key The key that's sending the event.
* @param eventType The type of event to send.
*/
fun sendAccessibilityEventForKey(key: Key, eventType: Int) {
val event = createAccessibilityEvent(key, eventType)
mAccessibilityUtils.requestSendAccessibilityEvent(event)
}
/**
* Returns the context-specific description for a [Key].
*
* @param key The key to describe.
* @return The context-specific description of the key.
*/
private fun getKeyDescription(key: Key): String? {
val editorInfo = mKeyboard!!.mId.mEditorInfo
val shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo)
val currentSettings = Settings.getInstance().current
val keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
mKeyboardView!!.context, mKeyboard, key, shouldObscure)
return if (currentSettings.isWordSeparator(key.code)) {
mAccessibilityUtils.getAutoCorrectionDescription(
keyCodeDescription, shouldObscure)
} else keyCodeDescription
}
/**
* Updates the parent's on-screen location.
*/
private fun updateParentLocation() {
mKeyboardView!!.getLocationOnScreen(mParentLocation)
}
companion object {
private val TAG = KeyboardAccessibilityNodeProvider::class.java.simpleName
// From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}.
private const val UNDEFINED = Int.MAX_VALUE
}
init {
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.Companion.instance
mAccessibilityUtils = AccessibilityUtils.Companion.instance
mKeyboardView = keyboardView
mDelegate = delegate
// Since this class is constructed lazily, we might not get a subsequent
// call to setKeyboard() and therefore need to call it now.
setKeyboard(keyboardView!!.keyboard)
}
}

View file

@ -1,301 +0,0 @@
/*
* 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 org.dslul.openboard.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 org.dslul.openboard.inputmethod.keyboard.Key;
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView;
import org.dslul.openboard.inputmethod.keyboard.PointerTracker;
import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.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() {
if (mLastKeyboardMode != KEYBOARD_IS_HIDDEN) {
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.getRawSubtype());
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);
downEvent.recycle();
// Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
tracker.onLongPressed();
// 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);
}
}
}
}

View file

@ -0,0 +1,255 @@
package org.dslul.openboard.inputmethod.accessibility
import android.graphics.Rect
import android.os.SystemClock
import android.util.Log
import android.util.SparseIntArray
import android.view.MotionEvent
import org.dslul.openboard.inputmethod.accessibility.AccessibilityLongPressTimer.LongPressTimerCallback
import org.dslul.openboard.inputmethod.keyboard.*
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils
/**
* This class represents a delegate that can be registered in [MainKeyboardView] to enhance
* accessibility support via composition rather via inheritance.
*/
class MainKeyboardAccessibilityDelegate(mainKeyboardView: MainKeyboardView,
keyDetector: KeyDetector) : KeyboardAccessibilityDelegate<MainKeyboardView?>(mainKeyboardView, keyDetector), LongPressTimerCallback {
companion object {
private val TAG = MainKeyboardAccessibilityDelegate::class.java.simpleName
/** Map of keyboard modes to resource IDs. */
private val KEYBOARD_MODE_RES_IDS = SparseIntArray()
private const val KEYBOARD_IS_HIDDEN = -1
init {
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 var mLastKeyboardMode = KEYBOARD_IS_HIDDEN
// The rectangle region to ignore hover events.
private val mBoundsToIgnoreHoverEvent = Rect()
private val mAccessibilityLongPressTimer: AccessibilityLongPressTimer// Since this method is called even when accessibility is off, make sure
// to check the state before announcing anything.
// Announce the language name only when the language is changed.
// Announce the mode only when the mode is changed.
// Announce the keyboard type only when the type is changed.
/**
* {@inheritDoc}
*/
override var keyboard: Keyboard?
get() = super.keyboard
set(keyboard) {
if (keyboard == null) {
return
}
val lastKeyboard = super.keyboard
super.keyboard = keyboard
val 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.instance.isAccessibilityEnabled) {
return
}
// Announce the language name only when the language is changed.
if (lastKeyboard == null || keyboard.mId.mSubtype != 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.
*/
fun onHideWindow() {
if (mLastKeyboardMode != KEYBOARD_IS_HIDDEN) {
announceKeyboardHidden()
}
mLastKeyboardMode = KEYBOARD_IS_HIDDEN
}
/**
* Announces which language of keyboard is being displayed.
*
* @param keyboard The new keyboard.
*/
private fun announceKeyboardLanguage(keyboard: Keyboard) {
val languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
keyboard.mId.mSubtype.rawSubtype)
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 fun announceKeyboardMode(keyboard: Keyboard) {
val context = mKeyboardView!!.context
val modeTextResId = KEYBOARD_MODE_RES_IDS[keyboard.mId.mMode]
if (modeTextResId == 0) {
return
}
val modeText = context.getString(modeTextResId)
val 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 fun announceKeyboardType(keyboard: Keyboard, lastKeyboard: Keyboard) {
val lastElementId = lastKeyboard.mId.mElementId
val resId: Int
resId = when (keyboard.mId.mElementId) {
KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, 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
}
R.string.spoken_description_mode_alpha
}
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
}
R.string.spoken_description_shiftmode_on
}
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
}
R.string.spoken_description_shiftmode_locked
}
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_shiftmode_locked
KeyboardId.ELEMENT_SYMBOLS -> R.string.spoken_description_mode_symbol
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_mode_symbol_shift
KeyboardId.ELEMENT_PHONE -> R.string.spoken_description_mode_phone
KeyboardId.ELEMENT_PHONE_SYMBOLS -> R.string.spoken_description_mode_phone_shift
else -> return
}
sendWindowStateChanged(resId)
}
/**
* Announces that the keyboard has been hidden.
*/
private fun announceKeyboardHidden() {
sendWindowStateChanged(R.string.announce_keyboard_hidden)
}
override fun performClickOn(key: Key) {
val x = key.hitBox.centerX()
val y = key.hitBox.centerY()
if (KeyboardAccessibilityDelegate.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 fun onHoverEnterTo(key: Key) {
val x = key.hitBox.centerX()
val y = key.hitBox.centerY()
if (KeyboardAccessibilityDelegate.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 fun onHoverExitFrom(key: Key) {
val x = key.hitBox.centerX()
val y = key.hitBox.centerY()
if (KeyboardAccessibilityDelegate.DEBUG_HOVER) {
Log.d(TAG, "onHoverExitFrom: key=" + key
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
}
mAccessibilityLongPressTimer.cancelLongPress()
super.onHoverExitFrom(key)
}
override fun performLongClickOn(key: Key) {
if (KeyboardAccessibilityDelegate.Companion.DEBUG_HOVER) {
Log.d(TAG, "performLongClickOn: key=$key")
}
val tracker = PointerTracker.getPointerTracker(KeyboardAccessibilityDelegate.Companion.HOVER_EVENT_POINTER_ID)
val eventTime = SystemClock.uptimeMillis()
val x = key.hitBox.centerX()
val y = key.hitBox.centerY()
val downEvent = MotionEvent.obtain(
eventTime, eventTime, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat(), 0 /* metaState */)
// Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
tracker.processMotionEvent(downEvent, mKeyDetector)
downEvent.recycle()
// Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
tracker.onLongPressed()
// 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.hitBox)
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.
val codePointOfNoPanelAutoMoreKey = key.moreKeys!![0].mCode
val text: String = KeyCodeDescriptionMapper.instance.getDescriptionForCodePoint(
mKeyboardView!!.context, codePointOfNoPanelAutoMoreKey)!!
text?.let { sendWindowStateChanged(it) }
}
}
init {
mAccessibilityLongPressTimer = AccessibilityLongPressTimer(
this /* callback */, mainKeyboardView.context)
}
}

View file

@ -1,120 +0,0 @@
/*
* 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 org.dslul.openboard.inputmethod.accessibility;
import android.graphics.Rect;
import android.util.Log;
import android.view.MotionEvent;
import org.dslul.openboard.inputmethod.keyboard.Key;
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
import org.dslul.openboard.inputmethod.keyboard.MoreKeysKeyboardView;
import org.dslul.openboard.inputmethod.keyboard.PointerTracker;
/**
* This class represents a delegate that can be registered in {@link MoreKeysKeyboardView} to
* enhance accessibility support via composition rather via inheritance.
*/
public class MoreKeysKeyboardAccessibilityDelegate
extends KeyboardAccessibilityDelegate<MoreKeysKeyboardView> {
private static final String TAG = MoreKeysKeyboardAccessibilityDelegate.class.getSimpleName();
private final Rect mMoreKeysKeyboardValidBounds = new Rect();
private static final int CLOSING_INSET_IN_PIXEL = 1;
private int mOpenAnnounceResId;
private int mCloseAnnounceResId;
public MoreKeysKeyboardAccessibilityDelegate(final MoreKeysKeyboardView moreKeysKeyboardView,
final KeyDetector keyDetector) {
super(moreKeysKeyboardView, keyDetector);
}
public void setOpenAnnounce(final int resId) {
mOpenAnnounceResId = resId;
}
public void setCloseAnnounce(final int resId) {
mCloseAnnounceResId = resId;
}
public void onShowMoreKeysKeyboard() {
sendWindowStateChanged(mOpenAnnounceResId);
}
public void onDismissMoreKeysKeyboard() {
sendWindowStateChanged(mCloseAnnounceResId);
}
@Override
protected void onHoverEnter(final MotionEvent event) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event));
}
super.onHoverEnter(event);
final int actionIndex = event.getActionIndex();
final int x = (int)event.getX(actionIndex);
final int y = (int)event.getY(actionIndex);
final int pointerId = event.getPointerId(actionIndex);
final long eventTime = event.getEventTime();
mKeyboardView.onDownEvent(x, y, pointerId, eventTime);
}
@Override
protected void onHoverMove(final MotionEvent event) {
super.onHoverMove(event);
final int actionIndex = event.getActionIndex();
final int x = (int)event.getX(actionIndex);
final int y = (int)event.getY(actionIndex);
final int pointerId = event.getPointerId(actionIndex);
final long eventTime = event.getEventTime();
mKeyboardView.onMoveEvent(x, y, pointerId, eventTime);
}
@Override
protected void onHoverExit(final MotionEvent event) {
final Key lastKey = getLastHoverKey();
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
}
if (lastKey != null) {
super.onHoverExitFrom(lastKey);
}
setLastHoverKey(null);
final int actionIndex = event.getActionIndex();
final int x = (int)event.getX(actionIndex);
final int y = (int)event.getY(actionIndex);
final int pointerId = event.getPointerId(actionIndex);
final long eventTime = event.getEventTime();
// A hover exit event at one pixel width or height area on the edges of more keys keyboard
// are treated as closing.
mMoreKeysKeyboardValidBounds.set(0, 0, mKeyboardView.getWidth(), mKeyboardView.getHeight());
mMoreKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL);
if (mMoreKeysKeyboardValidBounds.contains(x, y)) {
// Invoke {@link MoreKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover
// exit event selects a key.
mKeyboardView.onUpEvent(x, y, pointerId, eventTime);
// TODO: Should fix this reference. This is a hack to clear the state of
// {@link PointerTracker}.
PointerTracker.dismissAllMoreKeysPanels();
return;
}
// Close the more keys keyboard.
// TODO: Should fix this reference. This is a hack to clear the state of
// {@link PointerTracker}.
PointerTracker.dismissAllMoreKeysPanels();
}
}

View file

@ -0,0 +1,94 @@
package org.dslul.openboard.inputmethod.accessibility
import android.graphics.Rect
import android.util.Log
import android.view.MotionEvent
import org.dslul.openboard.inputmethod.keyboard.KeyDetector
import org.dslul.openboard.inputmethod.keyboard.MoreKeysKeyboardView
import org.dslul.openboard.inputmethod.keyboard.PointerTracker
/**
* This class represents a delegate that can be registered in [MoreKeysKeyboardView] to
* enhance accessibility support via composition rather via inheritance.
*/
class MoreKeysKeyboardAccessibilityDelegate(moreKeysKeyboardView: MoreKeysKeyboardView,
keyDetector: KeyDetector) : KeyboardAccessibilityDelegate<MoreKeysKeyboardView?>(moreKeysKeyboardView, keyDetector) {
private val mMoreKeysKeyboardValidBounds = Rect()
private var mOpenAnnounceResId = 0
private var mCloseAnnounceResId = 0
fun setOpenAnnounce(resId: Int) {
mOpenAnnounceResId = resId
}
fun setCloseAnnounce(resId: Int) {
mCloseAnnounceResId = resId
}
fun onShowMoreKeysKeyboard() {
sendWindowStateChanged(mOpenAnnounceResId)
}
fun onDismissMoreKeysKeyboard() {
sendWindowStateChanged(mCloseAnnounceResId)
}
override fun onHoverEnter(event: MotionEvent) {
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event))
}
super.onHoverEnter(event)
val actionIndex = event.actionIndex
val x = event.getX(actionIndex).toInt()
val y = event.getY(actionIndex).toInt()
val pointerId = event.getPointerId(actionIndex)
val eventTime = event.eventTime
mKeyboardView!!.onDownEvent(x, y, pointerId, eventTime)
}
override fun onHoverMove(event: MotionEvent) {
super.onHoverMove(event)
val actionIndex = event.actionIndex
val x = event.getX(actionIndex).toInt()
val y = event.getY(actionIndex).toInt()
val pointerId = event.getPointerId(actionIndex)
val eventTime = event.eventTime
mKeyboardView!!.onMoveEvent(x, y, pointerId, eventTime)
}
override fun onHoverExit(event: MotionEvent) {
val lastKey = lastHoverKey
if (DEBUG_HOVER) {
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey)
}
if (lastKey != null) {
super.onHoverExitFrom(lastKey)
}
lastHoverKey = null
val actionIndex = event.actionIndex
val x = event.getX(actionIndex).toInt()
val y = event.getY(actionIndex).toInt()
val pointerId = event.getPointerId(actionIndex)
val eventTime = event.eventTime
// A hover exit event at one pixel width or height area on the edges of more keys keyboard
// are treated as closing.
mMoreKeysKeyboardValidBounds[0, 0, mKeyboardView!!.width] = mKeyboardView.height
mMoreKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL)
if (mMoreKeysKeyboardValidBounds.contains(x, y)) { // Invoke {@link MoreKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover
// exit event selects a key.
mKeyboardView.onUpEvent(x, y, pointerId, eventTime)
// TODO: Should fix this reference. This is a hack to clear the state of
// {@link PointerTracker}.
PointerTracker.dismissAllMoreKeysPanels()
return
}
// Close the more keys keyboard.
// TODO: Should fix this reference. This is a hack to clear the state of
// {@link PointerTracker}.
PointerTracker.dismissAllMoreKeysPanels()
}
companion object {
private val TAG = MoreKeysKeyboardAccessibilityDelegate::class.java.simpleName
private const val CLOSING_INSET_IN_PIXEL = 1
}
}

View file

@ -400,7 +400,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
mLanguageOnSpacebarTextSize = keyHeight * mLanguageOnSpacebarTextRatio;
if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
if (AccessibilityUtils.Companion.getInstance().isAccessibilityEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new MainKeyboardAccessibilityDelegate(this, mKeyDetector);
}
@ -742,7 +742,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
onDismissMoreKeysPanel();
final MainKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null
&& AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
&& AccessibilityUtils.Companion.getInstance().isAccessibilityEnabled()) {
accessibilityDelegate.onHideWindow();
}
}
@ -754,7 +754,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
public boolean onHoverEvent(final MotionEvent event) {
final MainKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null
&& AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
&& AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled()) {
return accessibilityDelegate.onHoverEvent(event);
}
return super.onHoverEvent(event);

View file

@ -105,7 +105,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
if (AccessibilityUtils.Companion.getInstance().isAccessibilityEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new MoreKeysKeyboardAccessibilityDelegate(
this, mKeyDetector);
@ -143,7 +143,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
controller.onShowMoreKeysPanel(this);
final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null
&& AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
&& AccessibilityUtils.Companion.getInstance().isAccessibilityEnabled()) {
accessibilityDelegate.onShowMoreKeysKeyboard();
}
}
@ -241,7 +241,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
}
final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null
&& AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
&& AccessibilityUtils.Companion.getInstance().isAccessibilityEnabled()) {
accessibilityDelegate.onDismissMoreKeysKeyboard();
}
mController.onDismissMoreKeysPanel();
@ -288,7 +288,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
public boolean onHoverEvent(final MotionEvent event) {
final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null
&& AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
&& AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled()) {
return accessibilityDelegate.onHoverEvent(event);
}
return super.onHoverEvent(event);

View file

@ -81,7 +81,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements
public void setKeyboard(final Keyboard keyboard) {
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
if (AccessibilityUtils.Companion.getInstance().isAccessibilityEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new KeyboardAccessibilityDelegate<>(this, mKeyDetector);
}
@ -105,7 +105,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements
final KeyboardAccessibilityDelegate<EmojiPageKeyboardView> accessibilityDelegate =
mAccessibilityDelegate;
if (accessibilityDelegate != null
&& AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
&& AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled()) {
return accessibilityDelegate.onHoverEvent(event);
}
return super.onHoverEvent(event);

View file

@ -29,7 +29,7 @@ public final class GestureEnabler {
mShouldHandleGesture = mMainDictionaryAvailable
&& mGestureHandlingEnabledByInputField
&& mGestureHandlingEnabledByUser
&& !AccessibilityUtils.getInstance().isTouchExplorationEnabled();
&& !AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled();
}
// Note that this method is called from a non-UI thread.

View file

@ -56,7 +56,7 @@ public final class InputView extends FrameLayout {
@Override
protected boolean dispatchHoverEvent(final MotionEvent event) {
if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()
if (AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled()
&& mMainKeyboardView.isShowingMoreKeysPanel()) {
// With accessibility mode on, discard hover events while a more keys keyboard is shown.
// The {@link MoreKeysKeyboard} receives hover events directly from the platform.

View file

@ -918,7 +918,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
switcher.getKeyboard());
// Forward this event to the accessibility utilities, if enabled.
final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
final AccessibilityUtils accessUtils = AccessibilityUtils.Companion.getInstance();
if (accessUtils.isTouchExplorationEnabled()) {
accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
}
@ -1623,7 +1623,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// Cache the auto-correction in accessibility code so we can speak it if the user
// touches a key that will insert it.
AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords);
AccessibilityUtils.Companion.getInstance().setAutoCorrection(suggestedWords);
}
// Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}

View file

@ -447,7 +447,7 @@ final class SuggestionStripLayoutHelper {
// With accessibility touch exploration on, <code>wordView</code> should be enabled even
// when it is empty to avoid announcing as "disabled".
wordView.setEnabled(!TextUtils.isEmpty(word)
|| AccessibilityUtils.getInstance().isTouchExplorationEnabled());
|| AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled());
return wordView;
}

View file

@ -376,7 +376,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
// upward. Further {@link MotionEvent}s will be delivered to
// {@link #onTouchEvent(MotionEvent)}.
mNeedsToTransformTouchEventToHoverEvent =
AccessibilityUtils.getInstance().isTouchExplorationEnabled();
AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled();
mIsDispatchingHoverEventToMoreSuggestions = false;
return true;
}