mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-20 14:19:08 +00:00
convert accessibility package to kotlin
This commit is contained in:
parent
a059a0798b
commit
ae424c7202
22 changed files with 1486 additions and 1799 deletions
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,365 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.accessibility;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
|
||||
import org.dslul.openboard.inputmethod.latin.R;
|
||||
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
final class KeyCodeDescriptionMapper {
|
||||
private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
|
||||
private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X";
|
||||
private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X";
|
||||
private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X";
|
||||
private static final String SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon";
|
||||
private static final String SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X";
|
||||
|
||||
// The resource ID of the string spoken for obscured keys
|
||||
private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot;
|
||||
|
||||
private static final KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
|
||||
|
||||
public static KeyCodeDescriptionMapper getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// Sparse array of spoken description resource IDs indexed by key codes
|
||||
private final SparseIntArray mKeyCodeMap = new SparseIntArray();
|
||||
|
||||
private KeyCodeDescriptionMapper() {
|
||||
// Special non-character codes defined in Keyboard
|
||||
mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space);
|
||||
mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete);
|
||||
mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return);
|
||||
mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings);
|
||||
mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift);
|
||||
mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic);
|
||||
mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
|
||||
mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab);
|
||||
mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH,
|
||||
R.string.spoken_description_language_switch);
|
||||
mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next);
|
||||
mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS,
|
||||
R.string.spoken_description_action_previous);
|
||||
mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji);
|
||||
// Because the upper-case and lower-case mappings of the following letters is depending on
|
||||
// the locale, the upper case descriptions should be defined here. The lower case
|
||||
// descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
|
||||
// U+0049: "I" LATIN CAPITAL LETTER I
|
||||
// U+0069: "i" LATIN SMALL LETTER I
|
||||
// U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE
|
||||
// U+0131: "ı" LATIN SMALL LETTER DOTLESS I
|
||||
mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049);
|
||||
mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized description of the action performed by a specified
|
||||
* key based on the current keyboard state.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param keyboard The keyboard on which the key resides.
|
||||
* @param key The key from which to obtain a description.
|
||||
* @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
|
||||
* @return a character sequence describing the action performed by pressing the key
|
||||
*/
|
||||
public String getDescriptionForKey(final Context context, final Keyboard keyboard,
|
||||
final Key key, final boolean shouldObscure) {
|
||||
final int code = key.getCode();
|
||||
|
||||
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
|
||||
final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
|
||||
if (description != null) {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
if (code == Constants.CODE_SHIFT) {
|
||||
return getDescriptionForShiftKey(context, keyboard);
|
||||
}
|
||||
|
||||
if (code == Constants.CODE_ENTER) {
|
||||
// The following function returns the correct description in all action and
|
||||
// regular enter cases, taking care of all modes.
|
||||
return getDescriptionForActionKey(context, keyboard, key);
|
||||
}
|
||||
|
||||
if (code == Constants.CODE_OUTPUT_TEXT) {
|
||||
final String outputText = key.getOutputText();
|
||||
final String description = getSpokenEmoticonDescription(context, outputText);
|
||||
return TextUtils.isEmpty(description) ? outputText : description;
|
||||
}
|
||||
|
||||
// Just attempt to speak the description.
|
||||
if (code != Constants.CODE_UNSPECIFIED) {
|
||||
// If the key description should be obscured, now is the time to do it.
|
||||
final boolean isDefinedNonCtrl = Character.isDefined(code)
|
||||
&& !Character.isISOControl(code);
|
||||
if (shouldObscure && isDefinedNonCtrl) {
|
||||
return context.getString(OBSCURED_KEY_RES_ID);
|
||||
}
|
||||
final String description = getDescriptionForCodePoint(context, code);
|
||||
if (description != null) {
|
||||
return description;
|
||||
}
|
||||
if (!TextUtils.isEmpty(key.getLabel())) {
|
||||
return key.getLabel();
|
||||
}
|
||||
return context.getString(R.string.spoken_description_unknown);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
|
||||
* key or {@code null} if there is not a description provided for the
|
||||
* current keyboard context.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param keyboard The keyboard on which the key resides.
|
||||
* @return a character sequence describing the action performed by pressing the key
|
||||
*/
|
||||
private static String getDescriptionForSwitchAlphaSymbol(final Context context,
|
||||
final Keyboard keyboard) {
|
||||
final KeyboardId keyboardId = keyboard.mId;
|
||||
final int elementId = keyboardId.mElementId;
|
||||
final int resId;
|
||||
|
||||
switch (elementId) {
|
||||
case KeyboardId.ELEMENT_ALPHABET:
|
||||
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
|
||||
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
|
||||
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
|
||||
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
|
||||
resId = R.string.spoken_description_to_symbol;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_SYMBOLS:
|
||||
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
|
||||
resId = R.string.spoken_description_to_alpha;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_PHONE:
|
||||
resId = R.string.spoken_description_to_symbol;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_PHONE_SYMBOLS:
|
||||
resId = R.string.spoken_description_to_numeric;
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Missing description for keyboard element ID:" + elementId);
|
||||
return null;
|
||||
}
|
||||
return context.getString(resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a context-sensitive description of the "Shift" key.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param keyboard The keyboard on which the key resides.
|
||||
* @return A context-sensitive description of the "Shift" key.
|
||||
*/
|
||||
private static String getDescriptionForShiftKey(final Context context,
|
||||
final Keyboard keyboard) {
|
||||
final KeyboardId keyboardId = keyboard.mId;
|
||||
final int elementId = keyboardId.mElementId;
|
||||
final int resId;
|
||||
|
||||
switch (elementId) {
|
||||
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
|
||||
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
|
||||
resId = R.string.spoken_description_caps_lock;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
|
||||
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
|
||||
resId = R.string.spoken_description_shift_shifted;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_SYMBOLS:
|
||||
resId = R.string.spoken_description_symbols_shift;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
|
||||
resId = R.string.spoken_description_symbols_shift_shifted;
|
||||
break;
|
||||
default:
|
||||
resId = R.string.spoken_description_shift;
|
||||
}
|
||||
return context.getString(resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a context-sensitive description of the "Enter" action key.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param keyboard The keyboard on which the key resides.
|
||||
* @param key The key to describe.
|
||||
* @return Returns a context-sensitive description of the "Enter" action key.
|
||||
*/
|
||||
private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard,
|
||||
final Key key) {
|
||||
final KeyboardId keyboardId = keyboard.mId;
|
||||
final int actionId = keyboardId.imeAction();
|
||||
final int resId;
|
||||
|
||||
// Always use the label, if available.
|
||||
if (!TextUtils.isEmpty(key.getLabel())) {
|
||||
return key.getLabel().trim();
|
||||
}
|
||||
|
||||
// Otherwise, use the action ID.
|
||||
switch (actionId) {
|
||||
case EditorInfo.IME_ACTION_SEARCH:
|
||||
resId = R.string.spoken_description_search;
|
||||
break;
|
||||
case EditorInfo.IME_ACTION_GO:
|
||||
resId = R.string.label_go_key;
|
||||
break;
|
||||
case EditorInfo.IME_ACTION_SEND:
|
||||
resId = R.string.label_send_key;
|
||||
break;
|
||||
case EditorInfo.IME_ACTION_NEXT:
|
||||
resId = R.string.label_next_key;
|
||||
break;
|
||||
case EditorInfo.IME_ACTION_DONE:
|
||||
resId = R.string.label_done_key;
|
||||
break;
|
||||
case EditorInfo.IME_ACTION_PREVIOUS:
|
||||
resId = R.string.label_previous_key;
|
||||
break;
|
||||
default:
|
||||
resId = R.string.spoken_description_return;
|
||||
}
|
||||
return context.getString(resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a localized character sequence describing what will happen when
|
||||
* the specified key is pressed based on its key code point.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param codePoint The code point from which to obtain a description.
|
||||
* @return a character sequence describing the code point.
|
||||
*/
|
||||
public String getDescriptionForCodePoint(final Context context, final int codePoint) {
|
||||
// If the key description should be obscured, now is the time to do it.
|
||||
final int index = mKeyCodeMap.indexOfKey(codePoint);
|
||||
if (index >= 0) {
|
||||
return context.getString(mKeyCodeMap.valueAt(index));
|
||||
}
|
||||
final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint);
|
||||
if (accentedLetter != null) {
|
||||
return accentedLetter;
|
||||
}
|
||||
// Here, <code>code</code> may be a base (non-accented) letter.
|
||||
final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint);
|
||||
if (unsupportedSymbol != null) {
|
||||
return unsupportedSymbol;
|
||||
}
|
||||
final String emojiDescription = getSpokenEmojiDescription(context, codePoint);
|
||||
if (emojiDescription != null) {
|
||||
return emojiDescription;
|
||||
}
|
||||
if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
|
||||
return StringUtils.newSingleCodePointString(codePoint);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Remove this method once TTS supports those accented letters' verbalization.
|
||||
private String getSpokenAccentedLetterDescription(final Context context, final int code) {
|
||||
final boolean isUpperCase = Character.isUpperCase(code);
|
||||
final int baseCode = isUpperCase ? Character.toLowerCase(code) : code;
|
||||
final int baseIndex = mKeyCodeMap.indexOfKey(baseCode);
|
||||
final int resId = (baseIndex >= 0) ? mKeyCodeMap.valueAt(baseIndex)
|
||||
: getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT);
|
||||
if (resId == 0) {
|
||||
return null;
|
||||
}
|
||||
final String spokenText = context.getString(resId);
|
||||
return isUpperCase ? context.getString(R.string.spoken_description_upper_case, spokenText)
|
||||
: spokenText;
|
||||
}
|
||||
|
||||
// TODO: Remove this method once TTS supports those symbols' verbalization.
|
||||
private String getSpokenSymbolDescription(final Context context, final int code) {
|
||||
final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT);
|
||||
if (resId == 0) {
|
||||
return null;
|
||||
}
|
||||
final String spokenText = context.getString(resId);
|
||||
if (!TextUtils.isEmpty(spokenText)) {
|
||||
return spokenText;
|
||||
}
|
||||
// If a translated description is empty, fall back to unknown symbol description.
|
||||
return context.getString(R.string.spoken_symbol_unknown);
|
||||
}
|
||||
|
||||
// TODO: Remove this method once TTS supports emoji verbalization.
|
||||
private String getSpokenEmojiDescription(final Context context, final int code) {
|
||||
final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT);
|
||||
if (resId == 0) {
|
||||
return null;
|
||||
}
|
||||
final String spokenText = context.getString(resId);
|
||||
if (!TextUtils.isEmpty(spokenText)) {
|
||||
return spokenText;
|
||||
}
|
||||
// If a translated description is empty, fall back to unknown emoji description.
|
||||
return context.getString(R.string.spoken_emoji_unknown);
|
||||
}
|
||||
|
||||
private int getSpokenDescriptionId(final Context context, final int code,
|
||||
final String resourceNameFormat) {
|
||||
final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code);
|
||||
final Resources resources = context.getResources();
|
||||
// Note that the resource package name may differ from the context package name.
|
||||
final String resourcePackageName = resources.getResourcePackageName(
|
||||
R.string.spoken_description_unknown);
|
||||
final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
|
||||
if (resId != 0) {
|
||||
mKeyCodeMap.append(code, resId);
|
||||
}
|
||||
return resId;
|
||||
}
|
||||
|
||||
// TODO: Remove this method once TTS supports emoticon verbalization.
|
||||
private static String getSpokenEmoticonDescription(final Context context,
|
||||
final String outputText) {
|
||||
final StringBuilder sb = new StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX);
|
||||
final int textLength = outputText.length();
|
||||
for (int index = 0; index < textLength; index = outputText.offsetByCodePoints(index, 1)) {
|
||||
final int codePoint = outputText.codePointAt(index);
|
||||
sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint));
|
||||
}
|
||||
final String resourceName = sb.toString();
|
||||
final Resources resources = context.getResources();
|
||||
// Note that the resource package name may differ from the context package name.
|
||||
final String resourcePackageName = resources.getResourcePackageName(
|
||||
R.string.spoken_description_unknown);
|
||||
final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
|
||||
return (resId == 0) ? null : resources.getString(resId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,288 @@
|
|||
package org.dslul.openboard.inputmethod.accessibility
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.util.SparseIntArray
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId
|
||||
import org.dslul.openboard.inputmethod.latin.R
|
||||
import org.dslul.openboard.inputmethod.latin.common.Constants
|
||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils
|
||||
import java.util.*
|
||||
|
||||
internal class KeyCodeDescriptionMapper private constructor() {
|
||||
// Sparse array of spoken description resource IDs indexed by key codes
|
||||
private val mKeyCodeMap = SparseIntArray()
|
||||
|
||||
/**
|
||||
* Returns the localized description of the action performed by a specified
|
||||
* key based on the current keyboard state.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param keyboard The keyboard on which the key resides.
|
||||
* @param key The key from which to obtain a description.
|
||||
* @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
|
||||
* @return a character sequence describing the action performed by pressing the key
|
||||
*/
|
||||
fun getDescriptionForKey(context: Context, keyboard: Keyboard?,
|
||||
key: Key, shouldObscure: Boolean): String? {
|
||||
val code = key.code
|
||||
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
|
||||
val description = getDescriptionForSwitchAlphaSymbol(context, keyboard)
|
||||
if (description != null) {
|
||||
return description
|
||||
}
|
||||
}
|
||||
if (code == Constants.CODE_SHIFT) {
|
||||
return getDescriptionForShiftKey(context, keyboard)
|
||||
}
|
||||
if (code == Constants.CODE_ENTER) { // The following function returns the correct description in all action and
|
||||
// regular enter cases, taking care of all modes.
|
||||
return getDescriptionForActionKey(context, keyboard, key)
|
||||
}
|
||||
if (code == Constants.CODE_OUTPUT_TEXT) {
|
||||
val outputText = key.outputText
|
||||
val description = getSpokenEmoticonDescription(context, outputText)
|
||||
return if (TextUtils.isEmpty(description)) outputText else description
|
||||
}
|
||||
// Just attempt to speak the description.
|
||||
if (code != Constants.CODE_UNSPECIFIED) { // If the key description should be obscured, now is the time to do it.
|
||||
val isDefinedNonCtrl = (Character.isDefined(code)
|
||||
&& !Character.isISOControl(code))
|
||||
if (shouldObscure && isDefinedNonCtrl) {
|
||||
return context.getString(OBSCURED_KEY_RES_ID)
|
||||
}
|
||||
val description = getDescriptionForCodePoint(context, code)
|
||||
if (description != null) {
|
||||
return description
|
||||
}
|
||||
return if (!TextUtils.isEmpty(key.label)) {
|
||||
key.label
|
||||
} else context.getString(R.string.spoken_description_unknown)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a localized character sequence describing what will happen when
|
||||
* the specified key is pressed based on its key code point.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param codePoint The code point from which to obtain a description.
|
||||
* @return a character sequence describing the code point.
|
||||
*/
|
||||
fun getDescriptionForCodePoint(context: Context, codePoint: Int): String? { // If the key description should be obscured, now is the time to do it.
|
||||
val index = mKeyCodeMap.indexOfKey(codePoint)
|
||||
if (index >= 0) {
|
||||
return context.getString(mKeyCodeMap.valueAt(index))
|
||||
}
|
||||
val accentedLetter = getSpokenAccentedLetterDescription(context, codePoint)
|
||||
if (accentedLetter != null) {
|
||||
return accentedLetter
|
||||
}
|
||||
// Here, <code>code</code> may be a base (non-accented) letter.
|
||||
val unsupportedSymbol = getSpokenSymbolDescription(context, codePoint)
|
||||
if (unsupportedSymbol != null) {
|
||||
return unsupportedSymbol
|
||||
}
|
||||
val emojiDescription = getSpokenEmojiDescription(context, codePoint)
|
||||
if (emojiDescription != null) {
|
||||
return emojiDescription
|
||||
}
|
||||
return if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
|
||||
StringUtils.newSingleCodePointString(codePoint)
|
||||
} else null
|
||||
}
|
||||
|
||||
// TODO: Remove this method once TTS supports those accented letters' verbalization.
|
||||
private fun getSpokenAccentedLetterDescription(context: Context, code: Int): String? {
|
||||
val isUpperCase = Character.isUpperCase(code)
|
||||
val baseCode = if (isUpperCase) Character.toLowerCase(code) else code
|
||||
val baseIndex = mKeyCodeMap.indexOfKey(baseCode)
|
||||
val resId = if (baseIndex >= 0) mKeyCodeMap.valueAt(baseIndex) else getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT)
|
||||
if (resId == 0) {
|
||||
return null
|
||||
}
|
||||
val spokenText = context.getString(resId)
|
||||
return if (isUpperCase) context.getString(R.string.spoken_description_upper_case, spokenText) else spokenText
|
||||
}
|
||||
|
||||
// TODO: Remove this method once TTS supports those symbols' verbalization.
|
||||
private fun getSpokenSymbolDescription(context: Context, code: Int): String? {
|
||||
val resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT)
|
||||
if (resId == 0) {
|
||||
return null
|
||||
}
|
||||
val spokenText = context.getString(resId)
|
||||
return if (!TextUtils.isEmpty(spokenText)) {
|
||||
spokenText
|
||||
} else context.getString(R.string.spoken_symbol_unknown)
|
||||
// If a translated description is empty, fall back to unknown symbol description.
|
||||
}
|
||||
|
||||
// TODO: Remove this method once TTS supports emoji verbalization.
|
||||
private fun getSpokenEmojiDescription(context: Context, code: Int): String? {
|
||||
val resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT)
|
||||
if (resId == 0) {
|
||||
return null
|
||||
}
|
||||
val spokenText = context.getString(resId)
|
||||
return if (!TextUtils.isEmpty(spokenText)) {
|
||||
spokenText
|
||||
} else context.getString(R.string.spoken_emoji_unknown)
|
||||
// If a translated description is empty, fall back to unknown emoji description.
|
||||
}
|
||||
|
||||
private fun getSpokenDescriptionId(context: Context, code: Int,
|
||||
resourceNameFormat: String): Int {
|
||||
val resourceName = String.format(Locale.ROOT, resourceNameFormat, code)
|
||||
val resources = context.resources
|
||||
// Note that the resource package name may differ from the context package name.
|
||||
val resourcePackageName = resources.getResourcePackageName(
|
||||
R.string.spoken_description_unknown)
|
||||
val resId = resources.getIdentifier(resourceName, "string", resourcePackageName)
|
||||
if (resId != 0) {
|
||||
mKeyCodeMap.append(code, resId)
|
||||
}
|
||||
return resId
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = KeyCodeDescriptionMapper::class.java.simpleName
|
||||
private const val SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X"
|
||||
private const val SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X"
|
||||
private const val SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X"
|
||||
private const val SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon"
|
||||
private const val SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X"
|
||||
// The resource ID of the string spoken for obscured keys
|
||||
private const val OBSCURED_KEY_RES_ID = R.string.spoken_description_dot
|
||||
val instance = KeyCodeDescriptionMapper()
|
||||
|
||||
/**
|
||||
* Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
|
||||
* key or `null` if there is not a description provided for the
|
||||
* current keyboard context.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param keyboard The keyboard on which the key resides.
|
||||
* @return a character sequence describing the action performed by pressing the key
|
||||
*/
|
||||
private fun getDescriptionForSwitchAlphaSymbol(context: Context,
|
||||
keyboard: Keyboard?): String? {
|
||||
val keyboardId = keyboard!!.mId
|
||||
val elementId = keyboardId.mElementId
|
||||
val resId: Int
|
||||
resId = when (elementId) {
|
||||
KeyboardId.ELEMENT_ALPHABET, KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_to_symbol
|
||||
KeyboardId.ELEMENT_SYMBOLS, KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_to_alpha
|
||||
KeyboardId.ELEMENT_PHONE -> R.string.spoken_description_to_symbol
|
||||
KeyboardId.ELEMENT_PHONE_SYMBOLS -> R.string.spoken_description_to_numeric
|
||||
else -> {
|
||||
Log.e(TAG, "Missing description for keyboard element ID:$elementId")
|
||||
return null
|
||||
}
|
||||
}
|
||||
return context.getString(resId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a context-sensitive description of the "Shift" key.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param keyboard The keyboard on which the key resides.
|
||||
* @return A context-sensitive description of the "Shift" key.
|
||||
*/
|
||||
private fun getDescriptionForShiftKey(context: Context,
|
||||
keyboard: Keyboard?): String {
|
||||
val keyboardId = keyboard!!.mId
|
||||
val elementId = keyboardId.mElementId
|
||||
val resId: Int
|
||||
resId = when (elementId) {
|
||||
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED, KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_caps_lock
|
||||
KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> R.string.spoken_description_shift_shifted
|
||||
KeyboardId.ELEMENT_SYMBOLS -> R.string.spoken_description_symbols_shift
|
||||
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_symbols_shift_shifted
|
||||
else -> R.string.spoken_description_shift
|
||||
}
|
||||
return context.getString(resId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a context-sensitive description of the "Enter" action key.
|
||||
*
|
||||
* @param context The package's context.
|
||||
* @param keyboard The keyboard on which the key resides.
|
||||
* @param key The key to describe.
|
||||
* @return Returns a context-sensitive description of the "Enter" action key.
|
||||
*/
|
||||
private fun getDescriptionForActionKey(context: Context, keyboard: Keyboard?,
|
||||
key: Key): String {
|
||||
val keyboardId = keyboard!!.mId
|
||||
val actionId = keyboardId.imeAction()
|
||||
val resId: Int
|
||||
// Always use the label, if available.
|
||||
if (!TextUtils.isEmpty(key.label)) {
|
||||
return key.label!!.trim { it <= ' ' }
|
||||
}
|
||||
resId = when (actionId) {
|
||||
EditorInfo.IME_ACTION_SEARCH -> R.string.spoken_description_search
|
||||
EditorInfo.IME_ACTION_GO -> R.string.label_go_key
|
||||
EditorInfo.IME_ACTION_SEND -> R.string.label_send_key
|
||||
EditorInfo.IME_ACTION_NEXT -> R.string.label_next_key
|
||||
EditorInfo.IME_ACTION_DONE -> R.string.label_done_key
|
||||
EditorInfo.IME_ACTION_PREVIOUS -> R.string.label_previous_key
|
||||
else -> R.string.spoken_description_return
|
||||
}
|
||||
return context.getString(resId)
|
||||
}
|
||||
|
||||
// TODO: Remove this method once TTS supports emoticon verbalization.
|
||||
private fun getSpokenEmoticonDescription(context: Context,
|
||||
outputText: String?): String? {
|
||||
val sb = StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX)
|
||||
val textLength = outputText!!.length
|
||||
var index = 0
|
||||
while (index < textLength) {
|
||||
val codePoint = outputText.codePointAt(index)
|
||||
sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint))
|
||||
index = outputText.offsetByCodePoints(index, 1)
|
||||
}
|
||||
val resourceName = sb.toString()
|
||||
val resources = context.resources
|
||||
// Note that the resource package name may differ from the context package name.
|
||||
val resourcePackageName = resources.getResourcePackageName(
|
||||
R.string.spoken_description_unknown)
|
||||
val resId = resources.getIdentifier(resourceName, "string", resourcePackageName)
|
||||
return if (resId == 0) null else resources.getString(resId)
|
||||
}
|
||||
}
|
||||
|
||||
init { // Special non-character codes defined in Keyboard
|
||||
mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space)
|
||||
mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete)
|
||||
mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return)
|
||||
mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings)
|
||||
mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift)
|
||||
mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic)
|
||||
mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol)
|
||||
mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab)
|
||||
mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH,
|
||||
R.string.spoken_description_language_switch)
|
||||
mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next)
|
||||
mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS,
|
||||
R.string.spoken_description_action_previous)
|
||||
mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji)
|
||||
// Because the upper-case and lower-case mappings of the following letters is depending on
|
||||
// the locale, the upper case descriptions should be defined here. The lower case
|
||||
// descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
|
||||
// U+0049: "I" LATIN CAPITAL LETTER I
|
||||
// U+0069: "i" LATIN SMALL LETTER I
|
||||
// U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE
|
||||
// U+0131: "ı" LATIN SMALL LETTER DOTLESS I
|
||||
mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049)
|
||||
mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130)
|
||||
}
|
||||
}
|
|
@ -1,327 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.accessibility;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import androidx.core.view.AccessibilityDelegateCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardView;
|
||||
|
||||
/**
|
||||
* This class represents a delegate that can be registered in a class that extends
|
||||
* {@link KeyboardView} to enhance accessibility support via composition rather via inheritance.
|
||||
*
|
||||
* To implement accessibility mode, the target keyboard view has to:<p>
|
||||
* - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view.
|
||||
* - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}.
|
||||
*
|
||||
* @param <KV> The keyboard view class type.
|
||||
*/
|
||||
public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
|
||||
extends AccessibilityDelegateCompat {
|
||||
private static final String TAG = KeyboardAccessibilityDelegate.class.getSimpleName();
|
||||
protected static final boolean DEBUG_HOVER = false;
|
||||
|
||||
protected final KV mKeyboardView;
|
||||
protected final KeyDetector mKeyDetector;
|
||||
private Keyboard mKeyboard;
|
||||
private KeyboardAccessibilityNodeProvider<KV> mAccessibilityNodeProvider;
|
||||
private Key mLastHoverKey;
|
||||
|
||||
public static final int HOVER_EVENT_POINTER_ID = 0;
|
||||
|
||||
public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) {
|
||||
super();
|
||||
mKeyboardView = keyboardView;
|
||||
mKeyDetector = keyDetector;
|
||||
|
||||
// Ensure that the view has an accessibility delegate.
|
||||
ViewCompat.setAccessibilityDelegate(keyboardView, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the keyboard layout changes.
|
||||
* <p>
|
||||
* <b>Note:</b> This method will be called even if accessibility is not
|
||||
* enabled.
|
||||
* @param keyboard The keyboard that is being set to the wrapping view.
|
||||
*/
|
||||
public void setKeyboard(final Keyboard keyboard) {
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
}
|
||||
if (mAccessibilityNodeProvider != null) {
|
||||
mAccessibilityNodeProvider.setKeyboard(keyboard);
|
||||
}
|
||||
mKeyboard = keyboard;
|
||||
}
|
||||
|
||||
protected final Keyboard getKeyboard() {
|
||||
return mKeyboard;
|
||||
}
|
||||
|
||||
protected final void setLastHoverKey(final Key key) {
|
||||
mLastHoverKey = key;
|
||||
}
|
||||
|
||||
protected final Key getLastHoverKey() {
|
||||
return mLastHoverKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a window state change event with the specified string resource id.
|
||||
*
|
||||
* @param resId The string resource id of the text to send with the event.
|
||||
*/
|
||||
protected void sendWindowStateChanged(final int resId) {
|
||||
if (resId == 0) {
|
||||
return;
|
||||
}
|
||||
final Context context = mKeyboardView.getContext();
|
||||
sendWindowStateChanged(context.getString(resId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a window state change event with the specified text.
|
||||
*
|
||||
* @param text The text to send with the event.
|
||||
*/
|
||||
protected void sendWindowStateChanged(final String text) {
|
||||
final AccessibilityEvent stateChange = AccessibilityEvent.obtain(
|
||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
||||
mKeyboardView.onInitializeAccessibilityEvent(stateChange);
|
||||
stateChange.getText().add(text);
|
||||
stateChange.setContentDescription(null);
|
||||
|
||||
final ViewParent parent = mKeyboardView.getParent();
|
||||
if (parent != null) {
|
||||
parent.requestSendAccessibilityEvent(mKeyboardView, stateChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK
|
||||
* version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
|
||||
* node hierarchy provider.
|
||||
*
|
||||
* @param host The host view for the provider.
|
||||
* @return The accessibility node provider for the current keyboard.
|
||||
*/
|
||||
@Override
|
||||
public KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider(final View host) {
|
||||
return getAccessibilityNodeProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A lazily-instantiated node provider for this view delegate.
|
||||
*/
|
||||
protected KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider() {
|
||||
// Instantiate the provide only when requested. Since the system
|
||||
// will call this method multiple times it is a good practice to
|
||||
// cache the provider instance.
|
||||
if (mAccessibilityNodeProvider == null) {
|
||||
mAccessibilityNodeProvider =
|
||||
new KeyboardAccessibilityNodeProvider<>(mKeyboardView, this);
|
||||
}
|
||||
return mAccessibilityNodeProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a key that a hover event is on.
|
||||
*
|
||||
* @param event The hover event.
|
||||
* @return key The key that the <code>event</code> is on.
|
||||
*/
|
||||
protected final Key getHoverKeyOf(final MotionEvent event) {
|
||||
final int actionIndex = event.getActionIndex();
|
||||
final int x = (int)event.getX(actionIndex);
|
||||
final int y = (int)event.getY(actionIndex);
|
||||
return mKeyDetector.detectHitKey(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
|
||||
*
|
||||
* @param event The hover event.
|
||||
* @return {@code true} if the event is handled.
|
||||
*/
|
||||
public boolean onHoverEvent(final MotionEvent event) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_HOVER_ENTER:
|
||||
onHoverEnter(event);
|
||||
break;
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
onHoverMove(event);
|
||||
break;
|
||||
case MotionEvent.ACTION_HOVER_EXIT:
|
||||
onHoverExit(event);
|
||||
break;
|
||||
default:
|
||||
Log.w(getClass().getSimpleName(), "Unknown hover event: " + event);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {@link MotionEvent#ACTION_HOVER_ENTER} event.
|
||||
*
|
||||
* @param event A hover enter event.
|
||||
*/
|
||||
protected void onHoverEnter(final MotionEvent event) {
|
||||
final Key key = getHoverKeyOf(event);
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnter: key=" + key);
|
||||
}
|
||||
if (key != null) {
|
||||
onHoverEnterTo(key);
|
||||
}
|
||||
setLastHoverKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {@link MotionEvent#ACTION_HOVER_MOVE} event.
|
||||
*
|
||||
* @param event A hover move event.
|
||||
*/
|
||||
protected void onHoverMove(final MotionEvent event) {
|
||||
final Key lastKey = getLastHoverKey();
|
||||
final Key key = getHoverKeyOf(event);
|
||||
if (key != lastKey) {
|
||||
if (lastKey != null) {
|
||||
onHoverExitFrom(lastKey);
|
||||
}
|
||||
if (key != null) {
|
||||
onHoverEnterTo(key);
|
||||
}
|
||||
}
|
||||
if (key != null) {
|
||||
onHoverMoveWithin(key);
|
||||
}
|
||||
setLastHoverKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {@link MotionEvent#ACTION_HOVER_EXIT} event.
|
||||
*
|
||||
* @param event A hover exit event.
|
||||
*/
|
||||
protected void onHoverExit(final MotionEvent event) {
|
||||
final Key lastKey = getLastHoverKey();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
|
||||
}
|
||||
if (lastKey != null) {
|
||||
onHoverExitFrom(lastKey);
|
||||
}
|
||||
final Key key = getHoverKeyOf(event);
|
||||
// Make sure we're not getting an EXIT event because the user slid
|
||||
// off the keyboard area, then force a key press.
|
||||
if (key != null) {
|
||||
performClickOn(key);
|
||||
onHoverExitFrom(key);
|
||||
}
|
||||
setLastHoverKey(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform click on a key.
|
||||
*
|
||||
* @param key A key to be registered.
|
||||
*/
|
||||
public void performClickOn(final Key key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "performClickOn: key=" + key);
|
||||
}
|
||||
simulateTouchEvent(MotionEvent.ACTION_DOWN, key);
|
||||
simulateTouchEvent(MotionEvent.ACTION_UP, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulating a touch event by injecting a synthesized touch event into {@link KeyboardView}.
|
||||
*
|
||||
* @param touchAction The action of the synthesizing touch event.
|
||||
* @param key The key that a synthesized touch event is on.
|
||||
*/
|
||||
private void simulateTouchEvent(final int touchAction, final Key key) {
|
||||
final int x = key.getHitBox().centerX();
|
||||
final int y = key.getHitBox().centerY();
|
||||
final long eventTime = SystemClock.uptimeMillis();
|
||||
final MotionEvent touchEvent = MotionEvent.obtain(
|
||||
eventTime, eventTime, touchAction, x, y, 0 /* metaState */);
|
||||
mKeyboardView.onTouchEvent(touchEvent);
|
||||
touchEvent.recycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a hover enter event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected void onHoverEnterTo(final Key key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnterTo: key=" + key);
|
||||
}
|
||||
key.onPressed();
|
||||
mKeyboardView.invalidateKey(key);
|
||||
final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider();
|
||||
provider.onHoverEnterTo(key);
|
||||
provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a hover move event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected void onHoverMoveWithin(final Key key) { }
|
||||
|
||||
/**
|
||||
* Handles a hover exit event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected void onHoverExitFrom(final Key key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExitFrom: key=" + key);
|
||||
}
|
||||
key.onReleased();
|
||||
mKeyboardView.invalidateKey(key);
|
||||
final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider();
|
||||
provider.onHoverExitFrom(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform long click on a key.
|
||||
*
|
||||
* @param key A key to be long pressed on.
|
||||
*/
|
||||
public void performLongClickOn(final Key key) {
|
||||
// A extended class should override this method to implement long press.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
package org.dslul.openboard.inputmethod.accessibility
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.core.view.AccessibilityDelegateCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyDetector
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardView
|
||||
|
||||
/**
|
||||
* This class represents a delegate that can be registered in a class that extends
|
||||
* [KeyboardView] to enhance accessibility support via composition rather via inheritance.
|
||||
*
|
||||
* To implement accessibility mode, the target keyboard view has to:
|
||||
*
|
||||
*
|
||||
* - Call [.setKeyboard] when a new keyboard is set to the keyboard view.
|
||||
* - Dispatch a hover event by calling [.onHoverEnter].
|
||||
*
|
||||
* @param <KV> The keyboard view class type.
|
||||
</KV> */
|
||||
open class KeyboardAccessibilityDelegate<KV : KeyboardView?>(protected val mKeyboardView: KV, protected val mKeyDetector: KeyDetector) : AccessibilityDelegateCompat() {
|
||||
private var mKeyboard: Keyboard? = null
|
||||
private var mAccessibilityNodeProvider: KeyboardAccessibilityNodeProvider<KV>? = null
|
||||
private var mLastHoverKey: Key? = null
|
||||
|
||||
|
||||
protected open var lastHoverKey: Key?
|
||||
get() = mLastHoverKey
|
||||
set(key) {
|
||||
mLastHoverKey = key
|
||||
}
|
||||
/**
|
||||
* Called when the keyboard layout changes.
|
||||
*
|
||||
*
|
||||
* **Note:** This method will be called even if accessibility is not
|
||||
* enabled.
|
||||
* @param keyboard The keyboard that is being set to the wrapping view.
|
||||
*/
|
||||
open var keyboard: Keyboard?
|
||||
get() = mKeyboard
|
||||
set(keyboard) {
|
||||
if (keyboard == null) {
|
||||
return
|
||||
}
|
||||
mAccessibilityNodeProvider!!.setKeyboard(keyboard)
|
||||
mKeyboard = keyboard
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a window state change event with the specified string resource id.
|
||||
*
|
||||
* @param resId The string resource id of the text to send with the event.
|
||||
*/
|
||||
protected fun sendWindowStateChanged(resId: Int) {
|
||||
if (resId == 0) {
|
||||
return
|
||||
}
|
||||
val context = mKeyboardView!!.context
|
||||
sendWindowStateChanged(context.getString(resId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a window state change event with the specified text.
|
||||
*
|
||||
* @param text The text to send with the event.
|
||||
*/
|
||||
protected fun sendWindowStateChanged(text: String?) {
|
||||
val stateChange = AccessibilityEvent.obtain(
|
||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
|
||||
mKeyboardView!!.onInitializeAccessibilityEvent(stateChange)
|
||||
stateChange.text.add(text)
|
||||
stateChange.contentDescription = null
|
||||
val parent = mKeyboardView.parent
|
||||
parent?.requestSendAccessibilityEvent(mKeyboardView, stateChange)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK
|
||||
* version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
|
||||
* node hierarchy provider.
|
||||
*
|
||||
* @param host The host view for the provider.
|
||||
* @return The accessibility node provider for the current keyboard.
|
||||
*/
|
||||
override fun getAccessibilityNodeProvider(host: View): KeyboardAccessibilityNodeProvider<KV> {
|
||||
return accessibilityNodeProvider
|
||||
}// Instantiate the provide only when requested. Since the system
|
||||
// will call this method multiple times it is a good practice to
|
||||
// cache the provider instance.
|
||||
|
||||
/**
|
||||
* @return A lazily-instantiated node provider for this view delegate.
|
||||
*/
|
||||
protected val accessibilityNodeProvider: KeyboardAccessibilityNodeProvider<KV>
|
||||
get() { // Instantiate the provide only when requested. Since the system
|
||||
// will call this method multiple times it is a good practice to
|
||||
// cache the provider instance.
|
||||
return mAccessibilityNodeProvider ?: KeyboardAccessibilityNodeProvider(mKeyboardView, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a key that a hover event is on.
|
||||
*
|
||||
* @param event The hover event.
|
||||
* @return key The key that the `event` is on.
|
||||
*/
|
||||
protected fun getHoverKeyOf(event: MotionEvent): Key {
|
||||
val actionIndex = event.actionIndex
|
||||
val x = event.getX(actionIndex).toInt()
|
||||
val y = event.getY(actionIndex).toInt()
|
||||
return mKeyDetector.detectHitKey(x, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
|
||||
*
|
||||
* @param event The hover event.
|
||||
* @return `true` if the event is handled.
|
||||
*/
|
||||
fun onHoverEvent(event: MotionEvent): Boolean {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_HOVER_ENTER -> onHoverEnter(event)
|
||||
MotionEvent.ACTION_HOVER_MOVE -> onHoverMove(event)
|
||||
MotionEvent.ACTION_HOVER_EXIT -> onHoverExit(event)
|
||||
else -> Log.w(javaClass.simpleName, "Unknown hover event: $event")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Process [MotionEvent.ACTION_HOVER_ENTER] event.
|
||||
*
|
||||
* @param event A hover enter event.
|
||||
*/
|
||||
protected open fun onHoverEnter(event: MotionEvent) {
|
||||
val key = getHoverKeyOf(event)
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnter: key=$key")
|
||||
}
|
||||
key?.let { onHoverEnterTo(it) }
|
||||
mLastHoverKey = key
|
||||
}
|
||||
|
||||
/**
|
||||
* Process [MotionEvent.ACTION_HOVER_MOVE] event.
|
||||
*
|
||||
* @param event A hover move event.
|
||||
*/
|
||||
protected open fun onHoverMove(event: MotionEvent) {
|
||||
val lastKey = mLastHoverKey
|
||||
val key = getHoverKeyOf(event)
|
||||
if (key !== lastKey) {
|
||||
lastKey?.let { onHoverExitFrom(it) }
|
||||
key?.let { onHoverEnterTo(it) }
|
||||
}
|
||||
key?.let { onHoverMoveWithin(it) }
|
||||
mLastHoverKey = key
|
||||
}
|
||||
|
||||
/**
|
||||
* Process [MotionEvent.ACTION_HOVER_EXIT] event.
|
||||
*
|
||||
* @param event A hover exit event.
|
||||
*/
|
||||
protected open fun onHoverExit(event: MotionEvent) {
|
||||
val lastKey = mLastHoverKey
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey)
|
||||
}
|
||||
lastKey?.let { onHoverExitFrom(it) }
|
||||
val key = getHoverKeyOf(event)
|
||||
// Make sure we're not getting an EXIT event because the user slid
|
||||
// off the keyboard area, then force a key press.
|
||||
performClickOn(key)
|
||||
onHoverExitFrom(key)
|
||||
mLastHoverKey = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform click on a key.
|
||||
*
|
||||
* @param key A key to be registered.
|
||||
*/
|
||||
open fun performClickOn(key: Key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "performClickOn: key=$key")
|
||||
}
|
||||
simulateTouchEvent(MotionEvent.ACTION_DOWN, key)
|
||||
simulateTouchEvent(MotionEvent.ACTION_UP, key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulating a touch event by injecting a synthesized touch event into [KeyboardView].
|
||||
*
|
||||
* @param touchAction The action of the synthesizing touch event.
|
||||
* @param key The key that a synthesized touch event is on.
|
||||
*/
|
||||
private fun simulateTouchEvent(touchAction: Int, key: Key) {
|
||||
val x = key.hitBox.centerX()
|
||||
val y = key.hitBox.centerY()
|
||||
val eventTime = SystemClock.uptimeMillis()
|
||||
val touchEvent = MotionEvent.obtain(
|
||||
eventTime, eventTime, touchAction, x.toFloat(), y.toFloat(), 0 /* metaState */)
|
||||
mKeyboardView!!.onTouchEvent(touchEvent)
|
||||
touchEvent.recycle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a hover enter event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected open fun onHoverEnterTo(key: Key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnterTo: key=$key")
|
||||
}
|
||||
key.onPressed()
|
||||
mKeyboardView!!.invalidateKey(key)
|
||||
val provider = accessibilityNodeProvider
|
||||
provider.onHoverEnterTo(key)
|
||||
provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a hover move event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected fun onHoverMoveWithin(key: Key?) {}
|
||||
|
||||
/**
|
||||
* Handles a hover exit event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected open fun onHoverExitFrom(key: Key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExitFrom: key=$key")
|
||||
}
|
||||
key.onReleased()
|
||||
mKeyboardView!!.invalidateKey(key)
|
||||
val provider = accessibilityNodeProvider
|
||||
provider.onHoverExitFrom(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform long click on a key.
|
||||
*
|
||||
* @param key A key to be long pressed on.
|
||||
*/
|
||||
open fun performLongClickOn(key: Key) { // A extended class should override this method to implement long press.
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = KeyboardAccessibilityDelegate::class.java.simpleName
|
||||
const val DEBUG_HOVER = false
|
||||
const val HOVER_EVENT_POINTER_ID = 0
|
||||
}
|
||||
|
||||
init {
|
||||
// Ensure that the view has an accessibility delegate.
|
||||
ViewCompat.setAccessibilityDelegate(mKeyboardView!!, this)
|
||||
}
|
||||
}
|
|
@ -1,342 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.accessibility;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityEventCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityRecordCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardView;
|
||||
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
||||
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
||||
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Exposes a virtual view sub-tree for {@link KeyboardView} and generates
|
||||
* {@link AccessibilityEvent}s for individual {@link Key}s.
|
||||
* <p>
|
||||
* A virtual sub-tree is composed of imaginary {@link View}s that are reported
|
||||
* as a part of the view hierarchy for accessibility purposes. This enables
|
||||
* custom views that draw complex content to report them selves as a tree of
|
||||
* virtual views, thus conveying their logical structure.
|
||||
* </p>
|
||||
*/
|
||||
final class KeyboardAccessibilityNodeProvider<KV extends KeyboardView>
|
||||
extends AccessibilityNodeProviderCompat {
|
||||
private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName();
|
||||
|
||||
// From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}.
|
||||
private static final int UNDEFINED = Integer.MAX_VALUE;
|
||||
|
||||
private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
|
||||
private final AccessibilityUtils mAccessibilityUtils;
|
||||
|
||||
/** Temporary rect used to calculate in-screen bounds. */
|
||||
private final Rect mTempBoundsInScreen = new Rect();
|
||||
|
||||
/** The parent view's cached on-screen location. */
|
||||
private final int[] mParentLocation = CoordinateUtils.newInstance();
|
||||
|
||||
/** The virtual view identifier for the focused node. */
|
||||
private int mAccessibilityFocusedView = UNDEFINED;
|
||||
|
||||
/** The virtual view identifier for the hovering node. */
|
||||
private int mHoveringNodeId = UNDEFINED;
|
||||
|
||||
/** The keyboard view to provide an accessibility node info. */
|
||||
private final KV mKeyboardView;
|
||||
/** The accessibility delegate. */
|
||||
private final KeyboardAccessibilityDelegate<KV> mDelegate;
|
||||
|
||||
/** The current keyboard. */
|
||||
private Keyboard mKeyboard;
|
||||
|
||||
public KeyboardAccessibilityNodeProvider(final KV keyboardView,
|
||||
final KeyboardAccessibilityDelegate<KV> delegate) {
|
||||
super();
|
||||
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
|
||||
mAccessibilityUtils = AccessibilityUtils.getInstance();
|
||||
mKeyboardView = keyboardView;
|
||||
mDelegate = delegate;
|
||||
|
||||
// Since this class is constructed lazily, we might not get a subsequent
|
||||
// call to setKeyboard() and therefore need to call it now.
|
||||
setKeyboard(keyboardView.getKeyboard());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the keyboard represented by this node provider.
|
||||
*
|
||||
* @param keyboard The keyboard that is being set to the keyboard view.
|
||||
*/
|
||||
public void setKeyboard(final Keyboard keyboard) {
|
||||
mKeyboard = keyboard;
|
||||
}
|
||||
|
||||
private Key getKeyOf(final int virtualViewId) {
|
||||
if (mKeyboard == null) {
|
||||
return null;
|
||||
}
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
// Use a virtual view id as an index of the sorted keys list.
|
||||
if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) {
|
||||
return sortedKeys.get(virtualViewId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getVirtualViewIdOf(final Key key) {
|
||||
if (mKeyboard == null) {
|
||||
return View.NO_ID;
|
||||
}
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
final int size = sortedKeys.size();
|
||||
for (int index = 0; index < size; index++) {
|
||||
if (sortedKeys.get(index) == key) {
|
||||
// Use an index of the sorted keys list as a virtual view id.
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return View.NO_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and populates an {@link AccessibilityEvent} for the specified key
|
||||
* and event type.
|
||||
*
|
||||
* @param key A key on the host keyboard view.
|
||||
* @param eventType The event type to create.
|
||||
* @return A populated {@link AccessibilityEvent} for the key.
|
||||
* @see AccessibilityEvent
|
||||
*/
|
||||
public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) {
|
||||
final int virtualViewId = getVirtualViewIdOf(key);
|
||||
final String keyDescription = getKeyDescription(key);
|
||||
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
|
||||
event.setPackageName(mKeyboardView.getContext().getPackageName());
|
||||
event.setClassName(key.getClass().getName());
|
||||
event.setContentDescription(keyDescription);
|
||||
event.setEnabled(true);
|
||||
final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
|
||||
record.setSource(mKeyboardView, virtualViewId);
|
||||
return event;
|
||||
}
|
||||
|
||||
public void onHoverEnterTo(final Key key) {
|
||||
final int id = getVirtualViewIdOf(key);
|
||||
if (id == View.NO_ID) {
|
||||
return;
|
||||
}
|
||||
// Start hovering on the key. Because our accessibility model is lift-to-type, we should
|
||||
// report the node info without click and long click actions to avoid unnecessary
|
||||
// announcements.
|
||||
mHoveringNodeId = id;
|
||||
// Invalidate the node info of the key.
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
|
||||
}
|
||||
|
||||
public void onHoverExitFrom(final Key key) {
|
||||
mHoveringNodeId = UNDEFINED;
|
||||
// Invalidate the node info of the key to be able to revert the change we have done
|
||||
// in {@link #onHoverEnterTo(Key)}.
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
|
||||
* view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
|
||||
* the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
|
||||
* <p>
|
||||
* A virtual descendant is an imaginary View that is reported as a part of
|
||||
* the view hierarchy for accessibility purposes. This enables custom views
|
||||
* that draw complex content to report them selves as a tree of virtual
|
||||
* views, thus conveying their logical structure.
|
||||
* </p>
|
||||
* <p>
|
||||
* The implementer is responsible for obtaining an accessibility node info
|
||||
* from the pool of reusable instances and setting the desired properties of
|
||||
* the node info before returning it.
|
||||
* </p>
|
||||
*
|
||||
* @param virtualViewId A client defined virtual view id.
|
||||
* @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host
|
||||
* View.
|
||||
* @see AccessibilityNodeInfoCompat
|
||||
*/
|
||||
@Override
|
||||
public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) {
|
||||
if (virtualViewId == UNDEFINED) {
|
||||
return null;
|
||||
}
|
||||
if (virtualViewId == View.NO_ID) {
|
||||
// We are requested to create an AccessibilityNodeInfo describing
|
||||
// this View, i.e. the root of the virtual sub-tree.
|
||||
final AccessibilityNodeInfoCompat rootInfo =
|
||||
AccessibilityNodeInfoCompat.obtain(mKeyboardView);
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo);
|
||||
updateParentLocation();
|
||||
|
||||
// Add the virtual children of the root View.
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
final int size = sortedKeys.size();
|
||||
for (int index = 0; index < size; index++) {
|
||||
final Key key = sortedKeys.get(index);
|
||||
if (key.isSpacer()) {
|
||||
continue;
|
||||
}
|
||||
// Use an index of the sorted keys list as a virtual view id.
|
||||
rootInfo.addChild(mKeyboardView, index);
|
||||
}
|
||||
return rootInfo;
|
||||
}
|
||||
|
||||
// Find the key that corresponds to the given virtual view id.
|
||||
final Key key = getKeyOf(virtualViewId);
|
||||
if (key == null) {
|
||||
Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
|
||||
return null;
|
||||
}
|
||||
final String keyDescription = getKeyDescription(key);
|
||||
final Rect boundsInParent = key.getHitBox();
|
||||
|
||||
// Calculate the key's in-screen bounds.
|
||||
mTempBoundsInScreen.set(boundsInParent);
|
||||
mTempBoundsInScreen.offset(
|
||||
CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation));
|
||||
final Rect boundsInScreen = mTempBoundsInScreen;
|
||||
|
||||
// Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
|
||||
final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
|
||||
info.setPackageName(mKeyboardView.getContext().getPackageName());
|
||||
info.setClassName(key.getClass().getName());
|
||||
info.setContentDescription(keyDescription);
|
||||
info.setBoundsInParent(boundsInParent);
|
||||
info.setBoundsInScreen(boundsInScreen);
|
||||
info.setParent(mKeyboardView);
|
||||
info.setSource(mKeyboardView, virtualViewId);
|
||||
info.setEnabled(key.isEnabled());
|
||||
info.setVisibleToUser(true);
|
||||
// Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key.
|
||||
// See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}.
|
||||
if (virtualViewId != mHoveringNodeId) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||
if (key.isLongPressEnabled()) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
|
||||
}
|
||||
}
|
||||
|
||||
if (mAccessibilityFocusedView == virtualViewId) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
|
||||
} else {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAction(final int virtualViewId, final int action,
|
||||
final Bundle arguments) {
|
||||
final Key key = getKeyOf(virtualViewId);
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
return performActionForKey(key, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the specified accessibility action for the given key.
|
||||
*
|
||||
* @param key The on which to perform the action.
|
||||
* @param action The action to perform.
|
||||
* @return The result of performing the action, or false if the action is not supported.
|
||||
*/
|
||||
boolean performActionForKey(final Key key, final int action) {
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
|
||||
mAccessibilityFocusedView = getVirtualViewIdOf(key);
|
||||
sendAccessibilityEventForKey(
|
||||
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||
return true;
|
||||
case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
|
||||
mAccessibilityFocusedView = UNDEFINED;
|
||||
sendAccessibilityEventForKey(
|
||||
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
|
||||
return true;
|
||||
case AccessibilityNodeInfoCompat.ACTION_CLICK:
|
||||
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
mDelegate.performClickOn(key);
|
||||
return true;
|
||||
case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
|
||||
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
|
||||
mDelegate.performLongClickOn(key);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an accessibility event for the given {@link Key}.
|
||||
*
|
||||
* @param key The key that's sending the event.
|
||||
* @param eventType The type of event to send.
|
||||
*/
|
||||
void sendAccessibilityEventForKey(final Key key, final int eventType) {
|
||||
final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
|
||||
mAccessibilityUtils.requestSendAccessibilityEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context-specific description for a {@link Key}.
|
||||
*
|
||||
* @param key The key to describe.
|
||||
* @return The context-specific description of the key.
|
||||
*/
|
||||
private String getKeyDescription(final Key key) {
|
||||
final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo;
|
||||
final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
|
||||
final SettingsValues currentSettings = Settings.getInstance().getCurrent();
|
||||
final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
|
||||
mKeyboardView.getContext(), mKeyboard, key, shouldObscure);
|
||||
if (currentSettings.isWordSeparator(key.getCode())) {
|
||||
return mAccessibilityUtils.getAutoCorrectionDescription(
|
||||
keyCodeDescription, shouldObscure);
|
||||
}
|
||||
return keyCodeDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the parent's on-screen location.
|
||||
*/
|
||||
private void updateParentLocation() {
|
||||
mKeyboardView.getLocationOnScreen(mParentLocation);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
package org.dslul.openboard.inputmethod.accessibility
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.accessibility.AccessibilityEventCompat
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
||||
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardView
|
||||
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils
|
||||
import org.dslul.openboard.inputmethod.latin.settings.Settings
|
||||
|
||||
/**
|
||||
* Exposes a virtual view sub-tree for [KeyboardView] and generates
|
||||
* [AccessibilityEvent]s for individual [Key]s.
|
||||
*
|
||||
*
|
||||
* A virtual sub-tree is composed of imaginary [View]s that are reported
|
||||
* as a part of the view hierarchy for accessibility purposes. This enables
|
||||
* custom views that draw complex content to report them selves as a tree of
|
||||
* virtual views, thus conveying their logical structure.
|
||||
*
|
||||
*/
|
||||
class KeyboardAccessibilityNodeProvider<KV : KeyboardView?>(keyboardView: KV,
|
||||
delegate: KeyboardAccessibilityDelegate<KV>) : AccessibilityNodeProviderCompat() {
|
||||
private val mKeyCodeDescriptionMapper: KeyCodeDescriptionMapper
|
||||
private val mAccessibilityUtils: AccessibilityUtils
|
||||
/** Temporary rect used to calculate in-screen bounds. */
|
||||
private val mTempBoundsInScreen = Rect()
|
||||
/** The parent view's cached on-screen location. */
|
||||
private val mParentLocation = CoordinateUtils.newInstance()
|
||||
/** The virtual view identifier for the focused node. */
|
||||
private var mAccessibilityFocusedView = UNDEFINED
|
||||
/** The virtual view identifier for the hovering node. */
|
||||
private var mHoveringNodeId = UNDEFINED
|
||||
/** The keyboard view to provide an accessibility node info. */
|
||||
private val mKeyboardView: KV
|
||||
/** The accessibility delegate. */
|
||||
private val mDelegate: KeyboardAccessibilityDelegate<KV>
|
||||
/** The current keyboard. */
|
||||
private var mKeyboard: Keyboard? = null
|
||||
|
||||
/**
|
||||
* Sets the keyboard represented by this node provider.
|
||||
*
|
||||
* @param keyboard The keyboard that is being set to the keyboard view.
|
||||
*/
|
||||
fun setKeyboard(keyboard: Keyboard?) {
|
||||
mKeyboard = keyboard
|
||||
}
|
||||
|
||||
private fun getKeyOf(virtualViewId: Int): Key? {
|
||||
if (mKeyboard == null) {
|
||||
return null
|
||||
}
|
||||
val sortedKeys = mKeyboard!!.sortedKeys
|
||||
// Use a virtual view id as an index of the sorted keys list.
|
||||
return if (virtualViewId >= 0 && virtualViewId < sortedKeys.size) {
|
||||
sortedKeys[virtualViewId]
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun getVirtualViewIdOf(key: Key): Int {
|
||||
if (mKeyboard == null) {
|
||||
return View.NO_ID
|
||||
}
|
||||
val sortedKeys = mKeyboard!!.sortedKeys
|
||||
val size = sortedKeys.size
|
||||
for (index in 0 until size) {
|
||||
if (sortedKeys[index] === key) { // Use an index of the sorted keys list as a virtual view id.
|
||||
return index
|
||||
}
|
||||
}
|
||||
return View.NO_ID
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and populates an [AccessibilityEvent] for the specified key
|
||||
* and event type.
|
||||
*
|
||||
* @param key A key on the host keyboard view.
|
||||
* @param eventType The event type to create.
|
||||
* @return A populated [AccessibilityEvent] for the key.
|
||||
* @see AccessibilityEvent
|
||||
*/
|
||||
fun createAccessibilityEvent(key: Key, eventType: Int): AccessibilityEvent {
|
||||
val virtualViewId = getVirtualViewIdOf(key)
|
||||
val keyDescription = getKeyDescription(key)
|
||||
val event = AccessibilityEvent.obtain(eventType)
|
||||
event.packageName = mKeyboardView!!.context.packageName
|
||||
event.className = key.javaClass.name
|
||||
event.contentDescription = keyDescription
|
||||
event.isEnabled = true
|
||||
val record = AccessibilityEventCompat.asRecord(event)
|
||||
record.setSource(mKeyboardView, virtualViewId)
|
||||
return event
|
||||
}
|
||||
|
||||
fun onHoverEnterTo(key: Key) {
|
||||
val id = getVirtualViewIdOf(key)
|
||||
if (id == View.NO_ID) {
|
||||
return
|
||||
}
|
||||
// Start hovering on the key. Because our accessibility model is lift-to-type, we should
|
||||
// report the node info without click and long click actions to avoid unnecessary
|
||||
// announcements.
|
||||
mHoveringNodeId = id
|
||||
// Invalidate the node info of the key.
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER)
|
||||
}
|
||||
|
||||
fun onHoverExitFrom(key: Key) {
|
||||
mHoveringNodeId = UNDEFINED
|
||||
// Invalidate the node info of the key to be able to revert the change we have done
|
||||
// in {@link #onHoverEnterTo(Key)}.
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED)
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [AccessibilityNodeInfoCompat] representing a virtual
|
||||
* view, i.e. a descendant of the host View, with the given `virtualViewId` or
|
||||
* the host View itself if `virtualViewId` equals to [View.NO_ID].
|
||||
*
|
||||
*
|
||||
* A virtual descendant is an imaginary View that is reported as a part of
|
||||
* the view hierarchy for accessibility purposes. This enables custom views
|
||||
* that draw complex content to report them selves as a tree of virtual
|
||||
* views, thus conveying their logical structure.
|
||||
*
|
||||
*
|
||||
*
|
||||
* The implementer is responsible for obtaining an accessibility node info
|
||||
* from the pool of reusable instances and setting the desired properties of
|
||||
* the node info before returning it.
|
||||
*
|
||||
*
|
||||
* @param virtualViewId A client defined virtual view id.
|
||||
* @return A populated [AccessibilityNodeInfoCompat] for a virtual descendant or the host
|
||||
* View.
|
||||
* @see AccessibilityNodeInfoCompat
|
||||
*/
|
||||
override fun createAccessibilityNodeInfo(virtualViewId: Int): AccessibilityNodeInfoCompat? {
|
||||
if (virtualViewId == UNDEFINED) {
|
||||
return null
|
||||
}
|
||||
if (virtualViewId == View.NO_ID) { // We are requested to create an AccessibilityNodeInfo describing
|
||||
// this View, i.e. the root of the virtual sub-tree.
|
||||
val rootInfo = AccessibilityNodeInfoCompat.obtain(mKeyboardView)
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView!!, rootInfo)
|
||||
updateParentLocation()
|
||||
// Add the virtual children of the root View.
|
||||
val sortedKeys = mKeyboard!!.sortedKeys
|
||||
val size = sortedKeys.size
|
||||
for (index in 0 until size) {
|
||||
val key = sortedKeys[index]
|
||||
if (key.isSpacer) {
|
||||
continue
|
||||
}
|
||||
// Use an index of the sorted keys list as a virtual view id.
|
||||
rootInfo.addChild(mKeyboardView, index)
|
||||
}
|
||||
return rootInfo
|
||||
}
|
||||
// Find the key that corresponds to the given virtual view id.
|
||||
val key = getKeyOf(virtualViewId)
|
||||
if (key == null) {
|
||||
Log.e(TAG, "Invalid virtual view ID: $virtualViewId")
|
||||
return null
|
||||
}
|
||||
val keyDescription = getKeyDescription(key)
|
||||
val boundsInParent = key.hitBox
|
||||
// Calculate the key's in-screen bounds.
|
||||
mTempBoundsInScreen.set(boundsInParent)
|
||||
mTempBoundsInScreen.offset(
|
||||
CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation))
|
||||
val boundsInScreen = mTempBoundsInScreen
|
||||
// Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
|
||||
val info = AccessibilityNodeInfoCompat.obtain()
|
||||
info.packageName = mKeyboardView!!.context.packageName
|
||||
info.className = key.javaClass.name
|
||||
info.contentDescription = keyDescription
|
||||
info.setBoundsInParent(boundsInParent)
|
||||
info.setBoundsInScreen(boundsInScreen)
|
||||
info.setParent(mKeyboardView)
|
||||
info.setSource(mKeyboardView, virtualViewId)
|
||||
info.isEnabled = key.isEnabled
|
||||
info.isVisibleToUser = true
|
||||
// Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key.
|
||||
// See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}.
|
||||
if (virtualViewId != mHoveringNodeId) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
|
||||
if (key.isLongPressEnabled) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK)
|
||||
}
|
||||
}
|
||||
if (mAccessibilityFocusedView == virtualViewId) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS)
|
||||
} else {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
override fun performAction(virtualViewId: Int, action: Int,
|
||||
arguments: Bundle): Boolean {
|
||||
val key = getKeyOf(virtualViewId) ?: return false
|
||||
return performActionForKey(key, action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the specified accessibility action for the given key.
|
||||
*
|
||||
* @param key The on which to perform the action.
|
||||
* @param action The action to perform.
|
||||
* @return The result of performing the action, or false if the action is not supported.
|
||||
*/
|
||||
fun performActionForKey(key: Key, action: Int): Boolean {
|
||||
return when (action) {
|
||||
AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS -> {
|
||||
mAccessibilityFocusedView = getVirtualViewIdOf(key)
|
||||
sendAccessibilityEventForKey(
|
||||
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
|
||||
true
|
||||
}
|
||||
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS -> {
|
||||
mAccessibilityFocusedView = UNDEFINED
|
||||
sendAccessibilityEventForKey(
|
||||
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)
|
||||
true
|
||||
}
|
||||
AccessibilityNodeInfoCompat.ACTION_CLICK -> {
|
||||
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED)
|
||||
mDelegate.performClickOn(key)
|
||||
true
|
||||
}
|
||||
AccessibilityNodeInfoCompat.ACTION_LONG_CLICK -> {
|
||||
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED)
|
||||
mDelegate.performLongClickOn(key)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an accessibility event for the given [Key].
|
||||
*
|
||||
* @param key The key that's sending the event.
|
||||
* @param eventType The type of event to send.
|
||||
*/
|
||||
fun sendAccessibilityEventForKey(key: Key, eventType: Int) {
|
||||
val event = createAccessibilityEvent(key, eventType)
|
||||
mAccessibilityUtils.requestSendAccessibilityEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context-specific description for a [Key].
|
||||
*
|
||||
* @param key The key to describe.
|
||||
* @return The context-specific description of the key.
|
||||
*/
|
||||
private fun getKeyDescription(key: Key): String? {
|
||||
val editorInfo = mKeyboard!!.mId.mEditorInfo
|
||||
val shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo)
|
||||
val currentSettings = Settings.getInstance().current
|
||||
val keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
|
||||
mKeyboardView!!.context, mKeyboard, key, shouldObscure)
|
||||
return if (currentSettings.isWordSeparator(key.code)) {
|
||||
mAccessibilityUtils.getAutoCorrectionDescription(
|
||||
keyCodeDescription, shouldObscure)
|
||||
} else keyCodeDescription
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the parent's on-screen location.
|
||||
*/
|
||||
private fun updateParentLocation() {
|
||||
mKeyboardView!!.getLocationOnScreen(mParentLocation)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = KeyboardAccessibilityNodeProvider::class.java.simpleName
|
||||
// From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}.
|
||||
private const val UNDEFINED = Int.MAX_VALUE
|
||||
}
|
||||
|
||||
init {
|
||||
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.Companion.instance
|
||||
mAccessibilityUtils = AccessibilityUtils.Companion.instance
|
||||
mKeyboardView = keyboardView
|
||||
mDelegate = delegate
|
||||
// Since this class is constructed lazily, we might not get a subsequent
|
||||
// call to setKeyboard() and therefore need to call it now.
|
||||
setKeyboard(keyboardView!!.keyboard)
|
||||
}
|
||||
}
|
|
@ -1,301 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.accessibility;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
|
||||
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
|
||||
import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView;
|
||||
import org.dslul.openboard.inputmethod.keyboard.PointerTracker;
|
||||
import org.dslul.openboard.inputmethod.latin.R;
|
||||
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||
|
||||
/**
|
||||
* This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance
|
||||
* accessibility support via composition rather via inheritance.
|
||||
*/
|
||||
public final class MainKeyboardAccessibilityDelegate
|
||||
extends KeyboardAccessibilityDelegate<MainKeyboardView>
|
||||
implements AccessibilityLongPressTimer.LongPressTimerCallback {
|
||||
private static final String TAG = MainKeyboardAccessibilityDelegate.class.getSimpleName();
|
||||
|
||||
/** Map of keyboard modes to resource IDs. */
|
||||
private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray();
|
||||
|
||||
static {
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date);
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time);
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email);
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im);
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number);
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone);
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text);
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time);
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url);
|
||||
}
|
||||
|
||||
/** The most recently set keyboard mode. */
|
||||
private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
|
||||
private static final int KEYBOARD_IS_HIDDEN = -1;
|
||||
// The rectangle region to ignore hover events.
|
||||
private final Rect mBoundsToIgnoreHoverEvent = new Rect();
|
||||
|
||||
private final AccessibilityLongPressTimer mAccessibilityLongPressTimer;
|
||||
|
||||
public MainKeyboardAccessibilityDelegate(final MainKeyboardView mainKeyboardView,
|
||||
final KeyDetector keyDetector) {
|
||||
super(mainKeyboardView, keyDetector);
|
||||
mAccessibilityLongPressTimer = new AccessibilityLongPressTimer(
|
||||
this /* callback */, mainKeyboardView.getContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setKeyboard(final Keyboard keyboard) {
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
}
|
||||
final Keyboard lastKeyboard = getKeyboard();
|
||||
super.setKeyboard(keyboard);
|
||||
final int lastKeyboardMode = mLastKeyboardMode;
|
||||
mLastKeyboardMode = keyboard.mId.mMode;
|
||||
|
||||
// Since this method is called even when accessibility is off, make sure
|
||||
// to check the state before announcing anything.
|
||||
if (!AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Announce the language name only when the language is changed.
|
||||
if (lastKeyboard == null || !keyboard.mId.mSubtype.equals(lastKeyboard.mId.mSubtype)) {
|
||||
announceKeyboardLanguage(keyboard);
|
||||
return;
|
||||
}
|
||||
// Announce the mode only when the mode is changed.
|
||||
if (keyboard.mId.mMode != lastKeyboardMode) {
|
||||
announceKeyboardMode(keyboard);
|
||||
return;
|
||||
}
|
||||
// Announce the keyboard type only when the type is changed.
|
||||
if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) {
|
||||
announceKeyboardType(keyboard, lastKeyboard);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the keyboard is hidden and accessibility is enabled.
|
||||
*/
|
||||
public void onHideWindow() {
|
||||
if (mLastKeyboardMode != KEYBOARD_IS_HIDDEN) {
|
||||
announceKeyboardHidden();
|
||||
}
|
||||
mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces which language of keyboard is being displayed.
|
||||
*
|
||||
* @param keyboard The new keyboard.
|
||||
*/
|
||||
private void announceKeyboardLanguage(final Keyboard keyboard) {
|
||||
final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
|
||||
keyboard.mId.mSubtype.getRawSubtype());
|
||||
sendWindowStateChanged(languageText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces which type of keyboard is being displayed.
|
||||
* If the keyboard type is unknown, no announcement is made.
|
||||
*
|
||||
* @param keyboard The new keyboard.
|
||||
*/
|
||||
private void announceKeyboardMode(final Keyboard keyboard) {
|
||||
final Context context = mKeyboardView.getContext();
|
||||
final int modeTextResId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode);
|
||||
if (modeTextResId == 0) {
|
||||
return;
|
||||
}
|
||||
final String modeText = context.getString(modeTextResId);
|
||||
final String text = context.getString(R.string.announce_keyboard_mode, modeText);
|
||||
sendWindowStateChanged(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces which type of keyboard is being displayed.
|
||||
*
|
||||
* @param keyboard The new keyboard.
|
||||
* @param lastKeyboard The last keyboard.
|
||||
*/
|
||||
private void announceKeyboardType(final Keyboard keyboard, final Keyboard lastKeyboard) {
|
||||
final int lastElementId = lastKeyboard.mId.mElementId;
|
||||
final int resId;
|
||||
switch (keyboard.mId.mElementId) {
|
||||
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
|
||||
case KeyboardId.ELEMENT_ALPHABET:
|
||||
if (lastElementId == KeyboardId.ELEMENT_ALPHABET
|
||||
|| lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
|
||||
// Transition between alphabet mode and automatic shifted mode should be silently
|
||||
// ignored because it can be determined by each key's talk back announce.
|
||||
return;
|
||||
}
|
||||
resId = R.string.spoken_description_mode_alpha;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
|
||||
if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
|
||||
// Resetting automatic shifted mode by pressing the shift key causes the transition
|
||||
// from automatic shifted to manual shifted that should be silently ignored.
|
||||
return;
|
||||
}
|
||||
resId = R.string.spoken_description_shiftmode_on;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
|
||||
if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) {
|
||||
// Resetting caps locked mode by pressing the shift key causes the transition
|
||||
// from shift locked to shift lock shifted that should be silently ignored.
|
||||
return;
|
||||
}
|
||||
resId = R.string.spoken_description_shiftmode_locked;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
|
||||
resId = R.string.spoken_description_shiftmode_locked;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_SYMBOLS:
|
||||
resId = R.string.spoken_description_mode_symbol;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
|
||||
resId = R.string.spoken_description_mode_symbol_shift;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_PHONE:
|
||||
resId = R.string.spoken_description_mode_phone;
|
||||
break;
|
||||
case KeyboardId.ELEMENT_PHONE_SYMBOLS:
|
||||
resId = R.string.spoken_description_mode_phone_shift;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
sendWindowStateChanged(resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces that the keyboard has been hidden.
|
||||
*/
|
||||
private void announceKeyboardHidden() {
|
||||
sendWindowStateChanged(R.string.announce_keyboard_hidden);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performClickOn(final Key key) {
|
||||
final int x = key.getHitBox().centerX();
|
||||
final int y = key.getHitBox().centerY();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "performClickOn: key=" + key
|
||||
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
|
||||
}
|
||||
if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
|
||||
// This hover exit event points to the key that should be ignored.
|
||||
// Clear the ignoring region to handle further hover events.
|
||||
mBoundsToIgnoreHoverEvent.setEmpty();
|
||||
return;
|
||||
}
|
||||
super.performClickOn(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverEnterTo(final Key key) {
|
||||
final int x = key.getHitBox().centerX();
|
||||
final int y = key.getHitBox().centerY();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnterTo: key=" + key
|
||||
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
|
||||
}
|
||||
mAccessibilityLongPressTimer.cancelLongPress();
|
||||
if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
|
||||
return;
|
||||
}
|
||||
// This hover enter event points to the key that isn't in the ignoring region.
|
||||
// Further hover events should be handled.
|
||||
mBoundsToIgnoreHoverEvent.setEmpty();
|
||||
super.onHoverEnterTo(key);
|
||||
if (key.isLongPressEnabled()) {
|
||||
mAccessibilityLongPressTimer.startLongPress(key);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverExitFrom(final Key key) {
|
||||
final int x = key.getHitBox().centerX();
|
||||
final int y = key.getHitBox().centerY();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExitFrom: key=" + key
|
||||
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
|
||||
}
|
||||
mAccessibilityLongPressTimer.cancelLongPress();
|
||||
super.onHoverExitFrom(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performLongClickOn(final Key key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "performLongClickOn: key=" + key);
|
||||
}
|
||||
final PointerTracker tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID);
|
||||
final long eventTime = SystemClock.uptimeMillis();
|
||||
final int x = key.getHitBox().centerX();
|
||||
final int y = key.getHitBox().centerY();
|
||||
final MotionEvent downEvent = MotionEvent.obtain(
|
||||
eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */);
|
||||
// Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
|
||||
tracker.processMotionEvent(downEvent, mKeyDetector);
|
||||
downEvent.recycle();
|
||||
// Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
|
||||
tracker.onLongPressed();
|
||||
// If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
|
||||
// or a key invokes IME switcher dialog, we should just ignore the next
|
||||
// {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
|
||||
// {@link PointerTracker} is in operation or not.
|
||||
if (tracker.isInOperation()) {
|
||||
// This long press shows a more keys keyboard and further hover events should be
|
||||
// handled.
|
||||
mBoundsToIgnoreHoverEvent.setEmpty();
|
||||
return;
|
||||
}
|
||||
// This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
|
||||
// We should ignore further hover events on this key.
|
||||
mBoundsToIgnoreHoverEvent.set(key.getHitBox());
|
||||
if (key.hasNoPanelAutoMoreKey()) {
|
||||
// This long press has registered a code point without showing a more keys keyboard.
|
||||
// We should talk back the code point if possible.
|
||||
final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode;
|
||||
final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint(
|
||||
mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey);
|
||||
if (text != null) {
|
||||
sendWindowStateChanged(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
package org.dslul.openboard.inputmethod.accessibility
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.util.SparseIntArray
|
||||
import android.view.MotionEvent
|
||||
import org.dslul.openboard.inputmethod.accessibility.AccessibilityLongPressTimer.LongPressTimerCallback
|
||||
import org.dslul.openboard.inputmethod.keyboard.*
|
||||
import org.dslul.openboard.inputmethod.latin.R
|
||||
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils
|
||||
|
||||
/**
|
||||
* This class represents a delegate that can be registered in [MainKeyboardView] to enhance
|
||||
* accessibility support via composition rather via inheritance.
|
||||
*/
|
||||
class MainKeyboardAccessibilityDelegate(mainKeyboardView: MainKeyboardView,
|
||||
keyDetector: KeyDetector) : KeyboardAccessibilityDelegate<MainKeyboardView?>(mainKeyboardView, keyDetector), LongPressTimerCallback {
|
||||
companion object {
|
||||
private val TAG = MainKeyboardAccessibilityDelegate::class.java.simpleName
|
||||
/** Map of keyboard modes to resource IDs. */
|
||||
private val KEYBOARD_MODE_RES_IDS = SparseIntArray()
|
||||
private const val KEYBOARD_IS_HIDDEN = -1
|
||||
|
||||
init {
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date)
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time)
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email)
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im)
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number)
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone)
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text)
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time)
|
||||
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url)
|
||||
}
|
||||
}
|
||||
|
||||
/** The most recently set keyboard mode. */
|
||||
private var mLastKeyboardMode = KEYBOARD_IS_HIDDEN
|
||||
// The rectangle region to ignore hover events.
|
||||
private val mBoundsToIgnoreHoverEvent = Rect()
|
||||
private val mAccessibilityLongPressTimer: AccessibilityLongPressTimer// Since this method is called even when accessibility is off, make sure
|
||||
// to check the state before announcing anything.
|
||||
// Announce the language name only when the language is changed.
|
||||
// Announce the mode only when the mode is changed.
|
||||
// Announce the keyboard type only when the type is changed.
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override var keyboard: Keyboard?
|
||||
get() = super.keyboard
|
||||
set(keyboard) {
|
||||
if (keyboard == null) {
|
||||
return
|
||||
}
|
||||
val lastKeyboard = super.keyboard
|
||||
super.keyboard = keyboard
|
||||
val lastKeyboardMode = mLastKeyboardMode
|
||||
mLastKeyboardMode = keyboard.mId.mMode
|
||||
// Since this method is called even when accessibility is off, make sure
|
||||
// to check the state before announcing anything.
|
||||
if (!AccessibilityUtils.instance.isAccessibilityEnabled) {
|
||||
return
|
||||
}
|
||||
// Announce the language name only when the language is changed.
|
||||
if (lastKeyboard == null || keyboard.mId.mSubtype != lastKeyboard.mId.mSubtype) {
|
||||
announceKeyboardLanguage(keyboard)
|
||||
return
|
||||
}
|
||||
// Announce the mode only when the mode is changed.
|
||||
if (keyboard.mId.mMode != lastKeyboardMode) {
|
||||
announceKeyboardMode(keyboard)
|
||||
return
|
||||
}
|
||||
// Announce the keyboard type only when the type is changed.
|
||||
if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) {
|
||||
announceKeyboardType(keyboard, lastKeyboard)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the keyboard is hidden and accessibility is enabled.
|
||||
*/
|
||||
fun onHideWindow() {
|
||||
if (mLastKeyboardMode != KEYBOARD_IS_HIDDEN) {
|
||||
announceKeyboardHidden()
|
||||
}
|
||||
mLastKeyboardMode = KEYBOARD_IS_HIDDEN
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces which language of keyboard is being displayed.
|
||||
*
|
||||
* @param keyboard The new keyboard.
|
||||
*/
|
||||
private fun announceKeyboardLanguage(keyboard: Keyboard) {
|
||||
val languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
|
||||
keyboard.mId.mSubtype.rawSubtype)
|
||||
sendWindowStateChanged(languageText)
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces which type of keyboard is being displayed.
|
||||
* If the keyboard type is unknown, no announcement is made.
|
||||
*
|
||||
* @param keyboard The new keyboard.
|
||||
*/
|
||||
private fun announceKeyboardMode(keyboard: Keyboard) {
|
||||
val context = mKeyboardView!!.context
|
||||
val modeTextResId = KEYBOARD_MODE_RES_IDS[keyboard.mId.mMode]
|
||||
if (modeTextResId == 0) {
|
||||
return
|
||||
}
|
||||
val modeText = context.getString(modeTextResId)
|
||||
val text = context.getString(R.string.announce_keyboard_mode, modeText)
|
||||
sendWindowStateChanged(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces which type of keyboard is being displayed.
|
||||
*
|
||||
* @param keyboard The new keyboard.
|
||||
* @param lastKeyboard The last keyboard.
|
||||
*/
|
||||
private fun announceKeyboardType(keyboard: Keyboard, lastKeyboard: Keyboard) {
|
||||
val lastElementId = lastKeyboard.mId.mElementId
|
||||
val resId: Int
|
||||
resId = when (keyboard.mId.mElementId) {
|
||||
KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET -> {
|
||||
if (lastElementId == KeyboardId.ELEMENT_ALPHABET
|
||||
|| lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) { // Transition between alphabet mode and automatic shifted mode should be silently
|
||||
// ignored because it can be determined by each key's talk back announce.
|
||||
return
|
||||
}
|
||||
R.string.spoken_description_mode_alpha
|
||||
}
|
||||
KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> {
|
||||
if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) { // Resetting automatic shifted mode by pressing the shift key causes the transition
|
||||
// from automatic shifted to manual shifted that should be silently ignored.
|
||||
return
|
||||
}
|
||||
R.string.spoken_description_shiftmode_on
|
||||
}
|
||||
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> {
|
||||
if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) { // Resetting caps locked mode by pressing the shift key causes the transition
|
||||
// from shift locked to shift lock shifted that should be silently ignored.
|
||||
return
|
||||
}
|
||||
R.string.spoken_description_shiftmode_locked
|
||||
}
|
||||
KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_shiftmode_locked
|
||||
KeyboardId.ELEMENT_SYMBOLS -> R.string.spoken_description_mode_symbol
|
||||
KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_mode_symbol_shift
|
||||
KeyboardId.ELEMENT_PHONE -> R.string.spoken_description_mode_phone
|
||||
KeyboardId.ELEMENT_PHONE_SYMBOLS -> R.string.spoken_description_mode_phone_shift
|
||||
else -> return
|
||||
}
|
||||
sendWindowStateChanged(resId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces that the keyboard has been hidden.
|
||||
*/
|
||||
private fun announceKeyboardHidden() {
|
||||
sendWindowStateChanged(R.string.announce_keyboard_hidden)
|
||||
}
|
||||
|
||||
override fun performClickOn(key: Key) {
|
||||
val x = key.hitBox.centerX()
|
||||
val y = key.hitBox.centerY()
|
||||
if (KeyboardAccessibilityDelegate.DEBUG_HOVER) {
|
||||
Log.d(TAG, "performClickOn: key=" + key
|
||||
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
|
||||
}
|
||||
if (mBoundsToIgnoreHoverEvent.contains(x, y)) { // This hover exit event points to the key that should be ignored.
|
||||
// Clear the ignoring region to handle further hover events.
|
||||
mBoundsToIgnoreHoverEvent.setEmpty()
|
||||
return
|
||||
}
|
||||
super.performClickOn(key)
|
||||
}
|
||||
|
||||
override fun onHoverEnterTo(key: Key) {
|
||||
val x = key.hitBox.centerX()
|
||||
val y = key.hitBox.centerY()
|
||||
if (KeyboardAccessibilityDelegate.DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnterTo: key=" + key
|
||||
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
|
||||
}
|
||||
mAccessibilityLongPressTimer.cancelLongPress()
|
||||
if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
|
||||
return
|
||||
}
|
||||
// This hover enter event points to the key that isn't in the ignoring region.
|
||||
// Further hover events should be handled.
|
||||
mBoundsToIgnoreHoverEvent.setEmpty()
|
||||
super.onHoverEnterTo(key)
|
||||
if (key.isLongPressEnabled) {
|
||||
mAccessibilityLongPressTimer.startLongPress(key)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHoverExitFrom(key: Key) {
|
||||
val x = key.hitBox.centerX()
|
||||
val y = key.hitBox.centerY()
|
||||
if (KeyboardAccessibilityDelegate.DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExitFrom: key=" + key
|
||||
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
|
||||
}
|
||||
mAccessibilityLongPressTimer.cancelLongPress()
|
||||
super.onHoverExitFrom(key)
|
||||
}
|
||||
|
||||
override fun performLongClickOn(key: Key) {
|
||||
if (KeyboardAccessibilityDelegate.Companion.DEBUG_HOVER) {
|
||||
Log.d(TAG, "performLongClickOn: key=$key")
|
||||
}
|
||||
val tracker = PointerTracker.getPointerTracker(KeyboardAccessibilityDelegate.Companion.HOVER_EVENT_POINTER_ID)
|
||||
val eventTime = SystemClock.uptimeMillis()
|
||||
val x = key.hitBox.centerX()
|
||||
val y = key.hitBox.centerY()
|
||||
val downEvent = MotionEvent.obtain(
|
||||
eventTime, eventTime, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat(), 0 /* metaState */)
|
||||
// Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
|
||||
tracker.processMotionEvent(downEvent, mKeyDetector)
|
||||
downEvent.recycle()
|
||||
// Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
|
||||
tracker.onLongPressed()
|
||||
// If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
|
||||
// or a key invokes IME switcher dialog, we should just ignore the next
|
||||
// {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
|
||||
// {@link PointerTracker} is in operation or not.
|
||||
if (tracker.isInOperation) { // This long press shows a more keys keyboard and further hover events should be
|
||||
// handled.
|
||||
mBoundsToIgnoreHoverEvent.setEmpty()
|
||||
return
|
||||
}
|
||||
// This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
|
||||
// We should ignore further hover events on this key.
|
||||
mBoundsToIgnoreHoverEvent.set(key.hitBox)
|
||||
if (key.hasNoPanelAutoMoreKey()) { // This long press has registered a code point without showing a more keys keyboard.
|
||||
// We should talk back the code point if possible.
|
||||
val codePointOfNoPanelAutoMoreKey = key.moreKeys!![0].mCode
|
||||
val text: String = KeyCodeDescriptionMapper.instance.getDescriptionForCodePoint(
|
||||
mKeyboardView!!.context, codePointOfNoPanelAutoMoreKey)!!
|
||||
text?.let { sendWindowStateChanged(it) }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
mAccessibilityLongPressTimer = AccessibilityLongPressTimer(
|
||||
this /* callback */, mainKeyboardView.context)
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dslul.openboard.inputmethod.accessibility;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
|
||||
import org.dslul.openboard.inputmethod.keyboard.MoreKeysKeyboardView;
|
||||
import org.dslul.openboard.inputmethod.keyboard.PointerTracker;
|
||||
|
||||
/**
|
||||
* This class represents a delegate that can be registered in {@link MoreKeysKeyboardView} to
|
||||
* enhance accessibility support via composition rather via inheritance.
|
||||
*/
|
||||
public class MoreKeysKeyboardAccessibilityDelegate
|
||||
extends KeyboardAccessibilityDelegate<MoreKeysKeyboardView> {
|
||||
private static final String TAG = MoreKeysKeyboardAccessibilityDelegate.class.getSimpleName();
|
||||
|
||||
private final Rect mMoreKeysKeyboardValidBounds = new Rect();
|
||||
private static final int CLOSING_INSET_IN_PIXEL = 1;
|
||||
private int mOpenAnnounceResId;
|
||||
private int mCloseAnnounceResId;
|
||||
|
||||
public MoreKeysKeyboardAccessibilityDelegate(final MoreKeysKeyboardView moreKeysKeyboardView,
|
||||
final KeyDetector keyDetector) {
|
||||
super(moreKeysKeyboardView, keyDetector);
|
||||
}
|
||||
|
||||
public void setOpenAnnounce(final int resId) {
|
||||
mOpenAnnounceResId = resId;
|
||||
}
|
||||
|
||||
public void setCloseAnnounce(final int resId) {
|
||||
mCloseAnnounceResId = resId;
|
||||
}
|
||||
|
||||
public void onShowMoreKeysKeyboard() {
|
||||
sendWindowStateChanged(mOpenAnnounceResId);
|
||||
}
|
||||
|
||||
public void onDismissMoreKeysKeyboard() {
|
||||
sendWindowStateChanged(mCloseAnnounceResId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverEnter(final MotionEvent event) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event));
|
||||
}
|
||||
super.onHoverEnter(event);
|
||||
final int actionIndex = event.getActionIndex();
|
||||
final int x = (int)event.getX(actionIndex);
|
||||
final int y = (int)event.getY(actionIndex);
|
||||
final int pointerId = event.getPointerId(actionIndex);
|
||||
final long eventTime = event.getEventTime();
|
||||
mKeyboardView.onDownEvent(x, y, pointerId, eventTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverMove(final MotionEvent event) {
|
||||
super.onHoverMove(event);
|
||||
final int actionIndex = event.getActionIndex();
|
||||
final int x = (int)event.getX(actionIndex);
|
||||
final int y = (int)event.getY(actionIndex);
|
||||
final int pointerId = event.getPointerId(actionIndex);
|
||||
final long eventTime = event.getEventTime();
|
||||
mKeyboardView.onMoveEvent(x, y, pointerId, eventTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverExit(final MotionEvent event) {
|
||||
final Key lastKey = getLastHoverKey();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
|
||||
}
|
||||
if (lastKey != null) {
|
||||
super.onHoverExitFrom(lastKey);
|
||||
}
|
||||
setLastHoverKey(null);
|
||||
final int actionIndex = event.getActionIndex();
|
||||
final int x = (int)event.getX(actionIndex);
|
||||
final int y = (int)event.getY(actionIndex);
|
||||
final int pointerId = event.getPointerId(actionIndex);
|
||||
final long eventTime = event.getEventTime();
|
||||
// A hover exit event at one pixel width or height area on the edges of more keys keyboard
|
||||
// are treated as closing.
|
||||
mMoreKeysKeyboardValidBounds.set(0, 0, mKeyboardView.getWidth(), mKeyboardView.getHeight());
|
||||
mMoreKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL);
|
||||
if (mMoreKeysKeyboardValidBounds.contains(x, y)) {
|
||||
// Invoke {@link MoreKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover
|
||||
// exit event selects a key.
|
||||
mKeyboardView.onUpEvent(x, y, pointerId, eventTime);
|
||||
// TODO: Should fix this reference. This is a hack to clear the state of
|
||||
// {@link PointerTracker}.
|
||||
PointerTracker.dismissAllMoreKeysPanels();
|
||||
return;
|
||||
}
|
||||
// Close the more keys keyboard.
|
||||
// TODO: Should fix this reference. This is a hack to clear the state of
|
||||
// {@link PointerTracker}.
|
||||
PointerTracker.dismissAllMoreKeysPanels();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package org.dslul.openboard.inputmethod.accessibility
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import org.dslul.openboard.inputmethod.keyboard.KeyDetector
|
||||
import org.dslul.openboard.inputmethod.keyboard.MoreKeysKeyboardView
|
||||
import org.dslul.openboard.inputmethod.keyboard.PointerTracker
|
||||
|
||||
/**
|
||||
* This class represents a delegate that can be registered in [MoreKeysKeyboardView] to
|
||||
* enhance accessibility support via composition rather via inheritance.
|
||||
*/
|
||||
class MoreKeysKeyboardAccessibilityDelegate(moreKeysKeyboardView: MoreKeysKeyboardView,
|
||||
keyDetector: KeyDetector) : KeyboardAccessibilityDelegate<MoreKeysKeyboardView?>(moreKeysKeyboardView, keyDetector) {
|
||||
private val mMoreKeysKeyboardValidBounds = Rect()
|
||||
private var mOpenAnnounceResId = 0
|
||||
private var mCloseAnnounceResId = 0
|
||||
fun setOpenAnnounce(resId: Int) {
|
||||
mOpenAnnounceResId = resId
|
||||
}
|
||||
|
||||
fun setCloseAnnounce(resId: Int) {
|
||||
mCloseAnnounceResId = resId
|
||||
}
|
||||
|
||||
fun onShowMoreKeysKeyboard() {
|
||||
sendWindowStateChanged(mOpenAnnounceResId)
|
||||
}
|
||||
|
||||
fun onDismissMoreKeysKeyboard() {
|
||||
sendWindowStateChanged(mCloseAnnounceResId)
|
||||
}
|
||||
|
||||
override fun onHoverEnter(event: MotionEvent) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event))
|
||||
}
|
||||
super.onHoverEnter(event)
|
||||
val actionIndex = event.actionIndex
|
||||
val x = event.getX(actionIndex).toInt()
|
||||
val y = event.getY(actionIndex).toInt()
|
||||
val pointerId = event.getPointerId(actionIndex)
|
||||
val eventTime = event.eventTime
|
||||
mKeyboardView!!.onDownEvent(x, y, pointerId, eventTime)
|
||||
}
|
||||
|
||||
override fun onHoverMove(event: MotionEvent) {
|
||||
super.onHoverMove(event)
|
||||
val actionIndex = event.actionIndex
|
||||
val x = event.getX(actionIndex).toInt()
|
||||
val y = event.getY(actionIndex).toInt()
|
||||
val pointerId = event.getPointerId(actionIndex)
|
||||
val eventTime = event.eventTime
|
||||
mKeyboardView!!.onMoveEvent(x, y, pointerId, eventTime)
|
||||
}
|
||||
|
||||
override fun onHoverExit(event: MotionEvent) {
|
||||
val lastKey = lastHoverKey
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey)
|
||||
}
|
||||
if (lastKey != null) {
|
||||
super.onHoverExitFrom(lastKey)
|
||||
}
|
||||
lastHoverKey = null
|
||||
val actionIndex = event.actionIndex
|
||||
val x = event.getX(actionIndex).toInt()
|
||||
val y = event.getY(actionIndex).toInt()
|
||||
val pointerId = event.getPointerId(actionIndex)
|
||||
val eventTime = event.eventTime
|
||||
// A hover exit event at one pixel width or height area on the edges of more keys keyboard
|
||||
// are treated as closing.
|
||||
mMoreKeysKeyboardValidBounds[0, 0, mKeyboardView!!.width] = mKeyboardView.height
|
||||
mMoreKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL)
|
||||
if (mMoreKeysKeyboardValidBounds.contains(x, y)) { // Invoke {@link MoreKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover
|
||||
// exit event selects a key.
|
||||
mKeyboardView.onUpEvent(x, y, pointerId, eventTime)
|
||||
// TODO: Should fix this reference. This is a hack to clear the state of
|
||||
// {@link PointerTracker}.
|
||||
PointerTracker.dismissAllMoreKeysPanels()
|
||||
return
|
||||
}
|
||||
// Close the more keys keyboard.
|
||||
// TODO: Should fix this reference. This is a hack to clear the state of
|
||||
// {@link PointerTracker}.
|
||||
PointerTracker.dismissAllMoreKeysPanels()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MoreKeysKeyboardAccessibilityDelegate::class.java.simpleName
|
||||
private const val CLOSING_INSET_IN_PIXEL = 1
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -81,7 +81,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements
|
|||
public void setKeyboard(final Keyboard keyboard) {
|
||||
super.setKeyboard(keyboard);
|
||||
mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
|
||||
if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
|
||||
if (AccessibilityUtils.Companion.getInstance().isAccessibilityEnabled()) {
|
||||
if (mAccessibilityDelegate == null) {
|
||||
mAccessibilityDelegate = new KeyboardAccessibilityDelegate<>(this, mKeyDetector);
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements
|
|||
final KeyboardAccessibilityDelegate<EmojiPageKeyboardView> accessibilityDelegate =
|
||||
mAccessibilityDelegate;
|
||||
if (accessibilityDelegate != null
|
||||
&& AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
|
||||
&& AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled()) {
|
||||
return accessibilityDelegate.onHoverEvent(event);
|
||||
}
|
||||
return super.onHoverEvent(event);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -447,7 +447,7 @@ final class SuggestionStripLayoutHelper {
|
|||
// With accessibility touch exploration on, <code>wordView</code> should be enabled even
|
||||
// when it is empty to avoid announcing as "disabled".
|
||||
wordView.setEnabled(!TextUtils.isEmpty(word)
|
||||
|| AccessibilityUtils.getInstance().isTouchExplorationEnabled());
|
||||
|| AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled());
|
||||
return wordView;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue