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.media.AudioManager; 21 import android.os.Build; 22 import android.os.SystemClock; 23 import android.provider.Settings; 24 import android.support.v4.view.accessibility.AccessibilityEventCompat; 25 import android.text.TextUtils; 26 import android.util.Log; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.ViewParent; 31 import android.view.accessibility.AccessibilityEvent; 32 import android.view.accessibility.AccessibilityManager; 33 import android.view.inputmethod.EditorInfo; 34 35 import com.android.inputmethod.compat.SettingsSecureCompatUtils; 36 import com.android.inputmethod.latin.R; 37 import com.android.inputmethod.latin.SuggestedWords; 38 import com.android.inputmethod.latin.utils.InputTypeUtils; 39 40 public final class AccessibilityUtils { 41 private static final String TAG = AccessibilityUtils.class.getSimpleName(); 42 private static final String CLASS = AccessibilityUtils.class.getClass().getName(); 43 private static final String PACKAGE = 44 AccessibilityUtils.class.getClass().getPackage().getName(); 45 46 private static final AccessibilityUtils sInstance = new AccessibilityUtils(); 47 48 private Context mContext; 49 private AccessibilityManager mAccessibilityManager; 50 private AudioManager mAudioManager; 51 52 /** The most recent auto-correction. */ 53 private String mAutoCorrectionWord; 54 55 /** The most recent typed word for auto-correction. */ 56 private String mTypedWord; 57 58 /* 59 * Setting this constant to {@code false} will disable all keyboard 60 * accessibility code, regardless of whether Accessibility is turned on in 61 * the system settings. It should ONLY be used in the event of an emergency. 62 */ 63 private static final boolean ENABLE_ACCESSIBILITY = true; 64 65 public static void init(final Context context) { 66 if (!ENABLE_ACCESSIBILITY) return; 67 68 // These only need to be initialized if the kill switch is off. 69 sInstance.initInternal(context); 70 } 71 72 public static AccessibilityUtils getInstance() { 73 return sInstance; 74 } 75 76 private AccessibilityUtils() { 77 // This class is not publicly instantiable. 78 } 79 80 private void initInternal(final Context context) { 81 mContext = context; 82 mAccessibilityManager = 83 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 84 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 85 } 86 87 /** 88 * Returns {@code true} if accessibility is enabled. Currently, this means 89 * that the kill switch is off and system accessibility is turned on. 90 * 91 * @return {@code true} if accessibility is enabled. 92 */ 93 public boolean isAccessibilityEnabled() { 94 return ENABLE_ACCESSIBILITY && mAccessibilityManager.isEnabled(); 95 } 96 97 /** 98 * Returns {@code true} if touch exploration is enabled. Currently, this 99 * means that the kill switch is off, the device supports touch exploration, 100 * and system accessibility is turned on. 101 * 102 * @return {@code true} if touch exploration is enabled. 103 */ 104 public boolean isTouchExplorationEnabled() { 105 return isAccessibilityEnabled() && mAccessibilityManager.isTouchExplorationEnabled(); 106 } 107 108 /** 109 * Returns {@true} if the provided event is a touch exploration (e.g. hover) 110 * event. This is used to determine whether the event should be processed by 111 * the touch exploration code within the keyboard. 112 * 113 * @param event The event to check. 114 * @return {@true} is the event is a touch exploration event 115 */ 116 public static boolean isTouchExplorationEvent(final MotionEvent event) { 117 final int action = event.getAction(); 118 return action == MotionEvent.ACTION_HOVER_ENTER 119 || action == MotionEvent.ACTION_HOVER_EXIT 120 || action == MotionEvent.ACTION_HOVER_MOVE; 121 } 122 123 /** 124 * Returns whether the device should obscure typed password characters. 125 * Typically this means speaking "dot" in place of non-control characters. 126 * 127 * @return {@code true} if the device should obscure password characters. 128 */ 129 @SuppressWarnings("deprecation") 130 public boolean shouldObscureInput(final EditorInfo editorInfo) { 131 if (editorInfo == null) return false; 132 133 // The user can optionally force speaking passwords. 134 if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) { 135 final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(), 136 SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0; 137 if (speakPassword) return false; 138 } 139 140 // Always speak if the user is listening through headphones. 141 if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) { 142 return false; 143 } 144 145 // Don't speak if the IME is connected to a password field. 146 return InputTypeUtils.isPasswordInputType(editorInfo.inputType); 147 } 148 149 /** 150 * Sets the current auto-correction word and typed word. These may be used 151 * to provide the user with a spoken description of what auto-correction 152 * will occur when a key is typed. 153 * 154 * @param suggestedWords the list of suggested auto-correction words 155 */ 156 public void setAutoCorrection(final SuggestedWords suggestedWords) { 157 if (suggestedWords.mWillAutoCorrect) { 158 mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); 159 final SuggestedWords.SuggestedWordInfo typedWordInfo = suggestedWords.mTypedWordInfo; 160 if (null == typedWordInfo) { 161 mTypedWord = null; 162 } else { 163 mTypedWord = typedWordInfo.mWord; 164 } 165 } else { 166 mAutoCorrectionWord = null; 167 mTypedWord = null; 168 } 169 } 170 171 /** 172 * Obtains a description for an auto-correction key, taking into account the 173 * currently typed word and auto-correction. 174 * 175 * @param keyCodeDescription spoken description of the key that will insert 176 * an auto-correction 177 * @param shouldObscure whether the key should be obscured 178 * @return a description including a description of the auto-correction, if 179 * needed 180 */ 181 public String getAutoCorrectionDescription( 182 final String keyCodeDescription, final boolean shouldObscure) { 183 if (!TextUtils.isEmpty(mAutoCorrectionWord)) { 184 if (!TextUtils.equals(mAutoCorrectionWord, mTypedWord)) { 185 if (shouldObscure) { 186 // This should never happen, but just in case... 187 return mContext.getString(R.string.spoken_auto_correct_obscured, 188 keyCodeDescription); 189 } 190 return mContext.getString(R.string.spoken_auto_correct, keyCodeDescription, 191 mTypedWord, mAutoCorrectionWord); 192 } 193 } 194 195 return keyCodeDescription; 196 } 197 198 /** 199 * Sends the specified text to the {@link AccessibilityManager} to be 200 * spoken. 201 * 202 * @param view The source view. 203 * @param text The text to speak. 204 */ 205 public void announceForAccessibility(final View view, final CharSequence text) { 206 if (!mAccessibilityManager.isEnabled()) { 207 Log.e(TAG, "Attempted to speak when accessibility was disabled!"); 208 return; 209 } 210 211 // The following is a hack to avoid using the heavy-weight TextToSpeech 212 // class. Instead, we're just forcing a fake AccessibilityEvent into 213 // the screen reader to make it speak. 214 final AccessibilityEvent event = AccessibilityEvent.obtain(); 215 216 event.setPackageName(PACKAGE); 217 event.setClassName(CLASS); 218 event.setEventTime(SystemClock.uptimeMillis()); 219 event.setEnabled(true); 220 event.getText().add(text); 221 222 // Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use 223 // announce events. 224 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 225 event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT); 226 } else { 227 event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED); 228 } 229 230 final ViewParent viewParent = view.getParent(); 231 if ((viewParent == null) || !(viewParent instanceof ViewGroup)) { 232 Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility"); 233 return; 234 } 235 236 viewParent.requestSendAccessibilityEvent(view, event); 237 } 238 239 /** 240 * Handles speaking the "connect a headset to hear passwords" notification 241 * when connecting to a password field. 242 * 243 * @param view The source view. 244 * @param editorInfo The input connection's editor info attribute. 245 * @param restarting Whether the connection is being restarted. 246 */ 247 public void onStartInputViewInternal(final View view, final EditorInfo editorInfo, 248 final boolean restarting) { 249 if (shouldObscureInput(editorInfo)) { 250 final CharSequence text = mContext.getText(R.string.spoken_use_headphones); 251 announceForAccessibility(view, text); 252 } 253 } 254 255 /** 256 * Sends the specified {@link AccessibilityEvent} if accessibility is 257 * enabled. No operation if accessibility is disabled. 258 * 259 * @param event The event to send. 260 */ 261 public void requestSendAccessibilityEvent(final AccessibilityEvent event) { 262 if (mAccessibilityManager.isEnabled()) { 263 mAccessibilityManager.sendAccessibilityEvent(event); 264 } 265 } 266 } 267