1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.accessibility; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.inputmethodservice.InputMethodService; 22 import android.media.AudioManager; 23 import android.os.SystemClock; 24 import android.provider.Settings; 25 import android.util.Log; 26 import android.view.MotionEvent; 27 import android.view.accessibility.AccessibilityEvent; 28 import android.view.accessibility.AccessibilityManager; 29 import android.view.inputmethod.EditorInfo; 30 31 import com.android.inputmethod.compat.AccessibilityManagerCompatWrapper; 32 import com.android.inputmethod.compat.AudioManagerCompatWrapper; 33 import com.android.inputmethod.compat.InputTypeCompatUtils; 34 import com.android.inputmethod.compat.MotionEventCompatUtils; 35 import com.android.inputmethod.compat.SettingsSecureCompatUtils; 36 import com.android.inputmethod.latin.R; 37 38 public class AccessibilityUtils { 39 private static final String TAG = AccessibilityUtils.class.getSimpleName(); 40 private static final String CLASS = AccessibilityUtils.class.getClass().getName(); 41 private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage() 42 .getName(); 43 44 private static final AccessibilityUtils sInstance = new AccessibilityUtils(); 45 46 private Context mContext; 47 private AccessibilityManager mAccessibilityManager; 48 private AccessibilityManagerCompatWrapper mCompatManager; 49 private AudioManagerCompatWrapper mAudioManager; 50 51 /* 52 * Setting this constant to {@code false} will disable all keyboard 53 * accessibility code, regardless of whether Accessibility is turned on in 54 * the system settings. It should ONLY be used in the event of an emergency. 55 */ 56 private static final boolean ENABLE_ACCESSIBILITY = true; 57 58 public static void init(InputMethodService inputMethod, SharedPreferences prefs) { 59 if (!ENABLE_ACCESSIBILITY) 60 return; 61 62 // These only need to be initialized if the kill switch is off. 63 sInstance.initInternal(inputMethod, prefs); 64 KeyCodeDescriptionMapper.init(inputMethod, prefs); 65 AccessibleInputMethodServiceProxy.init(inputMethod, prefs); 66 AccessibleKeyboardViewProxy.init(inputMethod, prefs); 67 } 68 69 public static AccessibilityUtils getInstance() { 70 return sInstance; 71 } 72 73 private AccessibilityUtils() { 74 // This class is not publicly instantiable. 75 } 76 77 private void initInternal(Context context, SharedPreferences prefs) { 78 mContext = context; 79 mAccessibilityManager = (AccessibilityManager) context 80 .getSystemService(Context.ACCESSIBILITY_SERVICE); 81 mCompatManager = new AccessibilityManagerCompatWrapper(mAccessibilityManager); 82 83 final AudioManager audioManager = (AudioManager) context 84 .getSystemService(Context.AUDIO_SERVICE); 85 mAudioManager = new AudioManagerCompatWrapper(audioManager); 86 } 87 88 /** 89 * Returns {@code true} if touch exploration is enabled. Currently, this 90 * means that the kill switch is off, the device supports touch exploration, 91 * and a spoken feedback service is turned on. 92 * 93 * @return {@code true} if touch exploration is enabled. 94 */ 95 public boolean isTouchExplorationEnabled() { 96 return ENABLE_ACCESSIBILITY 97 && mAccessibilityManager.isEnabled() 98 && mCompatManager.isTouchExplorationEnabled(); 99 } 100 101 /** 102 * Returns {@true} if the provided event is a touch exploration (e.g. hover) 103 * event. This is used to determine whether the event should be processed by 104 * the touch exploration code within the keyboard. 105 * 106 * @param event The event to check. 107 * @return {@true} is the event is a touch exploration event 108 */ 109 public boolean isTouchExplorationEvent(MotionEvent event) { 110 final int action = event.getAction(); 111 112 return action == MotionEventCompatUtils.ACTION_HOVER_ENTER 113 || action == MotionEventCompatUtils.ACTION_HOVER_EXIT 114 || action == MotionEventCompatUtils.ACTION_HOVER_MOVE; 115 } 116 117 /** 118 * Returns whether the device should obscure typed password characters. 119 * Typically this means speaking "dot" in place of non-control characters. 120 * 121 * @return {@code true} if the device should obscure password characters. 122 */ 123 public boolean shouldObscureInput(EditorInfo attribute) { 124 if (attribute == null) 125 return false; 126 127 // The user can optionally force speaking passwords. 128 if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) { 129 final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(), 130 SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0; 131 if (speakPassword) 132 return false; 133 } 134 135 // Always speak if the user is listening through headphones. 136 if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) 137 return false; 138 139 // Don't speak if the IME is connected to a password field. 140 return InputTypeCompatUtils.isPasswordInputType(attribute.inputType); 141 } 142 143 /** 144 * Sends the specified text to the {@link AccessibilityManager} to be 145 * spoken. 146 * 147 * @param text the text to speak 148 */ 149 public void speak(CharSequence text) { 150 if (!mAccessibilityManager.isEnabled()) { 151 Log.e(TAG, "Attempted to speak when accessibility was disabled!"); 152 return; 153 } 154 155 // The following is a hack to avoid using the heavy-weight TextToSpeech 156 // class. Instead, we're just forcing a fake AccessibilityEvent into 157 // the screen reader to make it speak. 158 final AccessibilityEvent event = AccessibilityEvent 159 .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 160 161 event.setPackageName(PACKAGE); 162 event.setClassName(CLASS); 163 event.setEventTime(SystemClock.uptimeMillis()); 164 event.setEnabled(true); 165 event.getText().add(text); 166 167 mAccessibilityManager.sendAccessibilityEvent(event); 168 } 169 170 /** 171 * Handles speaking the "connect a headset to hear passwords" notification 172 * when connecting to a password field. 173 * 174 * @param attribute The input connection's editor info attribute. 175 * @param restarting Whether the connection is being restarted. 176 */ 177 public void onStartInputViewInternal(EditorInfo attribute, boolean restarting) { 178 if (shouldObscureInput(attribute)) { 179 final CharSequence text = mContext.getText(R.string.spoken_use_headphones); 180 speak(text); 181 } 182 } 183 } 184