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 init(final Context context)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 getInstance()72 public static AccessibilityUtils getInstance() { 73 return sInstance; 74 } 75 AccessibilityUtils()76 private AccessibilityUtils() { 77 // This class is not publicly instantiable. 78 } 79 initInternal(final Context context)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 */ isAccessibilityEnabled()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 */ isTouchExplorationEnabled()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 */ isTouchExplorationEvent(final MotionEvent event)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") shouldObscureInput(final EditorInfo editorInfo)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 * @param typedWord the currently typed word 156 */ setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord)157 public void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { 158 if (suggestedWords.mWillAutoCorrect) { 159 mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); 160 mTypedWord = typedWord; 161 } else { 162 mAutoCorrectionWord = null; 163 mTypedWord = null; 164 } 165 } 166 167 /** 168 * Obtains a description for an auto-correction key, taking into account the 169 * currently typed word and auto-correction. 170 * 171 * @param keyCodeDescription spoken description of the key that will insert 172 * an auto-correction 173 * @param shouldObscure whether the key should be obscured 174 * @return a description including a description of the auto-correction, if 175 * needed 176 */ getAutoCorrectionDescription( final String keyCodeDescription, final boolean shouldObscure)177 public String getAutoCorrectionDescription( 178 final String keyCodeDescription, final boolean shouldObscure) { 179 if (!TextUtils.isEmpty(mAutoCorrectionWord)) { 180 if (!TextUtils.equals(mAutoCorrectionWord, mTypedWord)) { 181 if (shouldObscure) { 182 // This should never happen, but just in case... 183 return mContext.getString(R.string.spoken_auto_correct_obscured, 184 keyCodeDescription); 185 } 186 return mContext.getString(R.string.spoken_auto_correct, keyCodeDescription, 187 mTypedWord, mAutoCorrectionWord); 188 } 189 } 190 191 return keyCodeDescription; 192 } 193 194 /** 195 * Sends the specified text to the {@link AccessibilityManager} to be 196 * spoken. 197 * 198 * @param view The source view. 199 * @param text The text to speak. 200 */ announceForAccessibility(final View view, final CharSequence text)201 public void announceForAccessibility(final View view, final CharSequence text) { 202 if (!mAccessibilityManager.isEnabled()) { 203 Log.e(TAG, "Attempted to speak when accessibility was disabled!"); 204 return; 205 } 206 207 // The following is a hack to avoid using the heavy-weight TextToSpeech 208 // class. Instead, we're just forcing a fake AccessibilityEvent into 209 // the screen reader to make it speak. 210 final AccessibilityEvent event = AccessibilityEvent.obtain(); 211 212 event.setPackageName(PACKAGE); 213 event.setClassName(CLASS); 214 event.setEventTime(SystemClock.uptimeMillis()); 215 event.setEnabled(true); 216 event.getText().add(text); 217 218 // Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use 219 // announce events. 220 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 221 event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT); 222 } else { 223 event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED); 224 } 225 226 final ViewParent viewParent = view.getParent(); 227 if ((viewParent == null) || !(viewParent instanceof ViewGroup)) { 228 Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility"); 229 return; 230 } 231 232 viewParent.requestSendAccessibilityEvent(view, event); 233 } 234 235 /** 236 * Handles speaking the "connect a headset to hear passwords" notification 237 * when connecting to a password field. 238 * 239 * @param view The source view. 240 * @param editorInfo The input connection's editor info attribute. 241 * @param restarting Whether the connection is being restarted. 242 */ onStartInputViewInternal(final View view, final EditorInfo editorInfo, final boolean restarting)243 public void onStartInputViewInternal(final View view, final EditorInfo editorInfo, 244 final boolean restarting) { 245 if (shouldObscureInput(editorInfo)) { 246 final CharSequence text = mContext.getText(R.string.spoken_use_headphones); 247 announceForAccessibility(view, text); 248 } 249 } 250 251 /** 252 * Sends the specified {@link AccessibilityEvent} if accessibility is 253 * enabled. No operation if accessibility is disabled. 254 * 255 * @param event The event to send. 256 */ requestSendAccessibilityEvent(final AccessibilityEvent event)257 public void requestSendAccessibilityEvent(final AccessibilityEvent event) { 258 if (mAccessibilityManager.isEnabled()) { 259 mAccessibilityManager.sendAccessibilityEvent(event); 260 } 261 } 262 } 263