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