diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.java deleted file mode 100644 index 3843b26b..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.kt b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.kt new file mode 100644 index 00000000..77c546d6 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.java deleted file mode 100644 index 4bcbc4c1..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.kt b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.kt new file mode 100644 index 00000000..0ec3d308 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.java deleted file mode 100644 index 903e073f..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ /dev/null @@ -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 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); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.kt b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.kt new file mode 100644 index 00000000..6d3f9074 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.kt @@ -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 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.java deleted file mode 100644 index 57b83154..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.java +++ /dev/null @@ -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:

- * - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view. - * - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}. - * - * @param The keyboard view class type. - */ -public class KeyboardAccessibilityDelegate - 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 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. - *

- * Note: 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 getAccessibilityNodeProvider(final View host) { - return getAccessibilityNodeProvider(); - } - - /** - * @return A lazily-instantiated node provider for this view delegate. - */ - protected KeyboardAccessibilityNodeProvider 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 event 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 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 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. - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.kt b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.kt new file mode 100644 index 00000000..d029b7a9 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.kt @@ -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 The keyboard view class type. + */ +open class KeyboardAccessibilityDelegate(protected val mKeyboardView: KV, protected val mKeyDetector: KeyDetector) : AccessibilityDelegateCompat() { + private var mKeyboard: Keyboard? = null + private var mAccessibilityNodeProvider: KeyboardAccessibilityNodeProvider? = 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 { + 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 + 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) + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java deleted file mode 100644 index 138d1179..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java +++ /dev/null @@ -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. - *

- * 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. - *

- */ -final class KeyboardAccessibilityNodeProvider - 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 mDelegate; - - /** The current keyboard. */ - private Keyboard mKeyboard; - - public KeyboardAccessibilityNodeProvider(final KV keyboardView, - final KeyboardAccessibilityDelegate 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 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 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 virtualViewId or - * the host View itself if virtualViewId equals to {@link 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 {@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 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); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.kt b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.kt new file mode 100644 index 00000000..8aaca410 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.kt @@ -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(keyboardView: KV, + delegate: KeyboardAccessibilityDelegate) : 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 + /** 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java deleted file mode 100644 index 911f8a64..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java +++ /dev/null @@ -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 - 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); - } - } - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.kt b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.kt new file mode 100644 index 00000000..be9fc78b --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.kt @@ -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, 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java deleted file mode 100644 index ec98f54d..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.kt b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.kt new file mode 100644 index 00000000..3882b2fb --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.kt @@ -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, 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MainKeyboardView.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MainKeyboardView.java index abf830f3..d8133d82 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MainKeyboardView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MainKeyboardView.java @@ -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); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboardView.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboardView.java index 9ef44c1f..320df314 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -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); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java index 0cad6dda..c6b03c3a 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java @@ -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 accessibilityDelegate = mAccessibilityDelegate; if (accessibilityDelegate != null - && AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { + && AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled()) { return accessibilityDelegate.onHoverEvent(event); } return super.onHoverEvent(event); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/GestureEnabler.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/GestureEnabler.java index ff85751b..c4c5040d 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/GestureEnabler.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/internal/GestureEnabler.java @@ -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. diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/InputView.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/InputView.java index 1f367ba6..1c187272 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/InputView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/InputView.java @@ -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. diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java index e0f4ce3b..a07e0834 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java @@ -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} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java index b6cd0b97..b6d98810 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -447,7 +447,7 @@ final class SuggestionStripLayoutHelper { // With accessibility touch exploration on, wordView 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; } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripView.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripView.java index be1b8267..905bffed 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripView.java @@ -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; }