Fixed speaking keys when editing password fields

Bug: 5042681
Change-Id: Ic4523ec38b0faa2b6a91d476ea7af7e69404861c
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index 86a5630..1619451 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -20,12 +20,18 @@
 import android.content.SharedPreferences;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.inputmethodservice.InputMethodService;
+import android.media.AudioManager;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.EditorInfo;
 
 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.keyboard.Key;
 import com.android.inputmethod.keyboard.KeyDetector;
@@ -39,17 +45,19 @@
     // Delay in milliseconds between key press DOWN and UP events
     private static final long DELAY_KEY_PRESS = 10;
 
-    private int mScaledEdgeSlop;
+    private InputMethodService mInputMethod;
+    private FlickGestureDetector mGestureDetector;
     private LatinKeyboardBaseView mView;
     private AccessibleKeyboardActionListener mListener;
-    private FlickGestureDetector mGestureDetector;
+    private AudioManagerCompatWrapper mAudioManager;
 
+    private int mScaledEdgeSlop;
     private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
     private int mLastX = -1;
     private int mLastY = -1;
 
-    public static void init(Context context, SharedPreferences prefs) {
-        sInstance.initInternal(context, prefs);
+    public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
+        sInstance.initInternal(inputMethod, prefs);
         sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance();
     }
 
@@ -65,15 +73,36 @@
         // Not publicly instantiable.
     }
 
-    private void initInternal(Context context, SharedPreferences prefs) {
+    private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) {
         final Paint paint = new Paint();
         paint.setTextAlign(Paint.Align.LEFT);
         paint.setTextSize(14.0f);
         paint.setAntiAlias(true);
         paint.setColor(Color.YELLOW);
 
-        mGestureDetector = new KeyboardFlickGestureDetector(context);
-        mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
+        mInputMethod = inputMethod;
+        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,
@@ -90,8 +119,10 @@
             if (key == null)
                 break;
 
+            final boolean shouldObscure = shouldObscureInput();
             final CharSequence description = KeyCodeDescriptionMapper.getInstance()
-                    .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key);
+                    .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key,
+                            shouldObscure);
 
             if (description == null)
                 return false;
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index ec4287d..7302830 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -28,6 +28,9 @@
 import java.util.HashMap;
 
 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();
 
     // Map of key labels to spoken description resource IDs
@@ -118,10 +121,12 @@
      * @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 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) {
             final CharSequence description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
             if (description != null)
@@ -136,12 +141,12 @@
             } else if (label.length() == 1
                     || (keyboard.isManualTemporaryUpperCase() && !TextUtils
                             .isEmpty(key.mHintLabel))) {
-                return getDescriptionForKeyCode(context, keyboard, key);
+                return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
             } else {
                 return label;
             }
         } else if (key.mCode != Keyboard.CODE_DUMMY) {
-            return getDescriptionForKeyCode(context, keyboard, key);
+            return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
         }
 
         return null;
@@ -206,19 +211,29 @@
      * @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
      */
-    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);
 
         if (keyboard.isShiftLocked() && mShiftLockedKeyCodeMap.containsKey(code)) {
             return context.getString(mShiftLockedKeyCodeMap.get(code));
         } else if (keyboard.isShiftedOrShiftLocked() && mShiftedKeyCodeMap.containsKey(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));
-        } else if (Character.isDefined(code) && !Character.isISOControl(code)) {
+        } else if (isDefinedNonCtrl) {
             return Character.toString((char) code);
         } else {
             return context.getString(R.string.spoken_description_unknown, code);
diff --git a/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
new file mode 100644
index 0000000..b6c3e2a
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
@@ -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);
+    }
+}