Fixed speaking keys when editing password fields
Bug: 5042681 Change-Id: Ic4523ec38b0faa2b6a91d476ea7af7e69404861c
This commit is contained in:
parent
0a5345c7b6
commit
58e3f1065e
3 changed files with 114 additions and 14 deletions
|
@ -20,12 +20,18 @@ import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
|
import android.media.AudioManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.ViewConfiguration;
|
import android.view.ViewConfiguration;
|
||||||
import android.view.accessibility.AccessibilityEvent;
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
|
||||||
import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
|
import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
|
||||||
|
import com.android.inputmethod.compat.AudioManagerCompatWrapper;
|
||||||
|
import com.android.inputmethod.compat.EditorInfoCompatUtils;
|
||||||
|
import com.android.inputmethod.compat.InputTypeCompatUtils;
|
||||||
import com.android.inputmethod.compat.MotionEventCompatUtils;
|
import com.android.inputmethod.compat.MotionEventCompatUtils;
|
||||||
import com.android.inputmethod.keyboard.Key;
|
import com.android.inputmethod.keyboard.Key;
|
||||||
import com.android.inputmethod.keyboard.KeyDetector;
|
import com.android.inputmethod.keyboard.KeyDetector;
|
||||||
|
@ -39,17 +45,19 @@ public class AccessibleKeyboardViewProxy {
|
||||||
// Delay in milliseconds between key press DOWN and UP events
|
// Delay in milliseconds between key press DOWN and UP events
|
||||||
private static final long DELAY_KEY_PRESS = 10;
|
private static final long DELAY_KEY_PRESS = 10;
|
||||||
|
|
||||||
private int mScaledEdgeSlop;
|
private InputMethodService mInputMethod;
|
||||||
|
private FlickGestureDetector mGestureDetector;
|
||||||
private LatinKeyboardBaseView mView;
|
private LatinKeyboardBaseView mView;
|
||||||
private AccessibleKeyboardActionListener mListener;
|
private AccessibleKeyboardActionListener mListener;
|
||||||
private FlickGestureDetector mGestureDetector;
|
private AudioManagerCompatWrapper mAudioManager;
|
||||||
|
|
||||||
|
private int mScaledEdgeSlop;
|
||||||
private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
|
private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
|
||||||
private int mLastX = -1;
|
private int mLastX = -1;
|
||||||
private int mLastY = -1;
|
private int mLastY = -1;
|
||||||
|
|
||||||
public static void init(Context context, SharedPreferences prefs) {
|
public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
|
||||||
sInstance.initInternal(context, prefs);
|
sInstance.initInternal(inputMethod, prefs);
|
||||||
sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance();
|
sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,15 +73,36 @@ public class AccessibleKeyboardViewProxy {
|
||||||
// Not publicly instantiable.
|
// Not publicly instantiable.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initInternal(Context context, SharedPreferences prefs) {
|
private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) {
|
||||||
final Paint paint = new Paint();
|
final Paint paint = new Paint();
|
||||||
paint.setTextAlign(Paint.Align.LEFT);
|
paint.setTextAlign(Paint.Align.LEFT);
|
||||||
paint.setTextSize(14.0f);
|
paint.setTextSize(14.0f);
|
||||||
paint.setAntiAlias(true);
|
paint.setAntiAlias(true);
|
||||||
paint.setColor(Color.YELLOW);
|
paint.setColor(Color.YELLOW);
|
||||||
|
|
||||||
mGestureDetector = new KeyboardFlickGestureDetector(context);
|
mInputMethod = inputMethod;
|
||||||
mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
|
mGestureDetector = new KeyboardFlickGestureDetector(inputMethod);
|
||||||
|
mScaledEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop();
|
||||||
|
|
||||||
|
final AudioManager audioManager = (AudioManager) inputMethod
|
||||||
|
.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
mAudioManager = new AudioManagerCompatWrapper(audioManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} if the device should not speak text (eg. non-control) characters
|
||||||
|
*/
|
||||||
|
private boolean shouldObscureInput() {
|
||||||
|
// Always speak if the user is listening through headphones.
|
||||||
|
if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
final EditorInfo info = mInputMethod.getCurrentInputEditorInfo();
|
||||||
|
if (info == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Don't speak if the IME is connected to a password field.
|
||||||
|
return InputTypeCompatUtils.isPasswordInputType(info.inputType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event,
|
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event,
|
||||||
|
@ -90,8 +119,10 @@ public class AccessibleKeyboardViewProxy {
|
||||||
if (key == null)
|
if (key == null)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
final boolean shouldObscure = shouldObscureInput();
|
||||||
final CharSequence description = KeyCodeDescriptionMapper.getInstance()
|
final CharSequence description = KeyCodeDescriptionMapper.getInstance()
|
||||||
.getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key);
|
.getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key,
|
||||||
|
shouldObscure);
|
||||||
|
|
||||||
if (description == null)
|
if (description == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -28,6 +28,9 @@ import com.android.inputmethod.latin.R;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
public class KeyCodeDescriptionMapper {
|
public class KeyCodeDescriptionMapper {
|
||||||
|
// 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 KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
|
private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
|
||||||
|
|
||||||
// Map of key labels to spoken description resource IDs
|
// Map of key labels to spoken description resource IDs
|
||||||
|
@ -118,10 +121,12 @@ public class KeyCodeDescriptionMapper {
|
||||||
* @param context The package's context.
|
* @param context The package's context.
|
||||||
* @param keyboard The keyboard on which the key resides.
|
* @param keyboard The keyboard on which the key resides.
|
||||||
* @param key The key from which to obtain a description.
|
* @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
|
* @return a character sequence describing the action performed by pressing
|
||||||
* the key
|
* the key
|
||||||
*/
|
*/
|
||||||
public CharSequence getDescriptionForKey(Context context, Keyboard keyboard, Key key) {
|
public CharSequence getDescriptionForKey(Context context, Keyboard keyboard, Key key,
|
||||||
|
boolean shouldObscure) {
|
||||||
if (key.mCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
|
if (key.mCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
|
||||||
final CharSequence description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
|
final CharSequence description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
|
||||||
if (description != null)
|
if (description != null)
|
||||||
|
@ -136,12 +141,12 @@ public class KeyCodeDescriptionMapper {
|
||||||
} else if (label.length() == 1
|
} else if (label.length() == 1
|
||||||
|| (keyboard.isManualTemporaryUpperCase() && !TextUtils
|
|| (keyboard.isManualTemporaryUpperCase() && !TextUtils
|
||||||
.isEmpty(key.mHintLabel))) {
|
.isEmpty(key.mHintLabel))) {
|
||||||
return getDescriptionForKeyCode(context, keyboard, key);
|
return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
|
||||||
} else {
|
} else {
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
} else if (key.mCode != Keyboard.CODE_DUMMY) {
|
} else if (key.mCode != Keyboard.CODE_DUMMY) {
|
||||||
return getDescriptionForKeyCode(context, keyboard, key);
|
return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -206,19 +211,29 @@ public class KeyCodeDescriptionMapper {
|
||||||
* @param context The package's context.
|
* @param context The package's context.
|
||||||
* @param keyboard The keyboard on which the key resides.
|
* @param keyboard The keyboard on which the key resides.
|
||||||
* @param key The key from which to obtain a description.
|
* @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
|
* @return a character sequence describing the action performed by pressing
|
||||||
* the key
|
* the key
|
||||||
*/
|
*/
|
||||||
private CharSequence getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key) {
|
private CharSequence getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key,
|
||||||
|
boolean shouldObscure) {
|
||||||
final int code = getCorrectKeyCode(keyboard, key);
|
final int code = getCorrectKeyCode(keyboard, key);
|
||||||
|
|
||||||
if (keyboard.isShiftLocked() && mShiftLockedKeyCodeMap.containsKey(code)) {
|
if (keyboard.isShiftLocked() && mShiftLockedKeyCodeMap.containsKey(code)) {
|
||||||
return context.getString(mShiftLockedKeyCodeMap.get(code));
|
return context.getString(mShiftLockedKeyCodeMap.get(code));
|
||||||
} else if (keyboard.isShiftedOrShiftLocked() && mShiftedKeyCodeMap.containsKey(code)) {
|
} else if (keyboard.isShiftedOrShiftLocked() && mShiftedKeyCodeMap.containsKey(code)) {
|
||||||
return context.getString(mShiftedKeyCodeMap.get(code));
|
return context.getString(mShiftedKeyCodeMap.get(code));
|
||||||
} else if (mKeyCodeMap.containsKey(code)) {
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mKeyCodeMap.containsKey(code)) {
|
||||||
return context.getString(mKeyCodeMap.get(code));
|
return context.getString(mKeyCodeMap.get(code));
|
||||||
} else if (Character.isDefined(code) && !Character.isISOControl(code)) {
|
} else if (isDefinedNonCtrl) {
|
||||||
return Character.toString((char) code);
|
return Character.toString((char) code);
|
||||||
} else {
|
} else {
|
||||||
return context.getString(R.string.spoken_description_unknown, code);
|
return context.getString(R.string.spoken_description_unknown, code);
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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 com.android.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.media.AudioManager;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public class AudioManagerCompatWrapper {
|
||||||
|
private static final Method METHOD_isWiredHeadsetOn = CompatUtils.getMethod(
|
||||||
|
AudioManager.class, "isWiredHeadsetOn");
|
||||||
|
private static final Method METHOD_isBluetoothA2dpOn = CompatUtils.getMethod(
|
||||||
|
AudioManager.class, "isBluetoothA2dpOn");
|
||||||
|
|
||||||
|
private final AudioManager mManager;
|
||||||
|
|
||||||
|
public AudioManagerCompatWrapper(AudioManager manager) {
|
||||||
|
mManager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether audio routing to the wired headset is on or off.
|
||||||
|
*
|
||||||
|
* @return true if audio is being routed to/from wired headset;
|
||||||
|
* false if otherwise
|
||||||
|
*/
|
||||||
|
public boolean isWiredHeadsetOn() {
|
||||||
|
return (Boolean) CompatUtils.invoke(mManager, false, METHOD_isWiredHeadsetOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether A2DP audio routing to the Bluetooth headset is on or off.
|
||||||
|
*
|
||||||
|
* @return true if A2DP audio is being routed to/from Bluetooth headset;
|
||||||
|
* false if otherwise
|
||||||
|
*/
|
||||||
|
public boolean isBluetoothA2dpOn() {
|
||||||
|
return (Boolean) CompatUtils.invoke(mManager, false, METHOD_isBluetoothA2dpOn);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue