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