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.inputmethodservice.InputMethodService; 21 import android.media.AudioManager; 22 import android.os.Build; 23 import android.os.SystemClock; 24 import android.provider.Settings; 25 import android.support.v4.view.accessibility.AccessibilityEventCompat; 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.AudioManagerCompatWrapper; 36 import com.android.inputmethod.compat.SettingsSecureCompatUtils; 37 import com.android.inputmethod.latin.InputTypeUtils; 38 import com.android.inputmethod.latin.R; 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 = AccessibilityUtils.class.getClass().getPackage() 44 .getName(); 45 46 private static final AccessibilityUtils sInstance = new AccessibilityUtils(); 47 48 private Context mContext; 49 private AccessibilityManager mAccessibilityManager; 50 private AudioManagerCompatWrapper mAudioManager; 51 52 /* 53 * Setting this constant to {@code false} will disable all keyboard 54 * accessibility code, regardless of whether Accessibility is turned on in 55 * the system settings. It should ONLY be used in the event of an emergency. 56 */ 57 private static final boolean ENABLE_ACCESSIBILITY = true; 58 init(InputMethodService inputMethod)59 public static void init(InputMethodService inputMethod) { 60 if (!ENABLE_ACCESSIBILITY) 61 return; 62 63 // These only need to be initialized if the kill switch is off. 64 sInstance.initInternal(inputMethod); 65 KeyCodeDescriptionMapper.init(); 66 AccessibleKeyboardViewProxy.init(inputMethod); 67 } 68 getInstance()69 public static AccessibilityUtils getInstance() { 70 return sInstance; 71 } 72 AccessibilityUtils()73 private AccessibilityUtils() { 74 // This class is not publicly instantiable. 75 } 76 initInternal(Context context)77 private void initInternal(Context context) { 78 mContext = context; 79 mAccessibilityManager = (AccessibilityManager) context 80 .getSystemService(Context.ACCESSIBILITY_SERVICE); 81 82 final AudioManager audioManager = (AudioManager) context 83 .getSystemService(Context.AUDIO_SERVICE); 84 mAudioManager = new AudioManagerCompatWrapper(audioManager); 85 } 86 87 /** 88 * Returns {@code true} if touch exploration is enabled. Currently, this 89 * means that the kill switch is off, the device supports touch exploration, 90 * and a spoken feedback service is turned on. 91 * 92 * @return {@code true} if touch exploration is enabled. 93 */ isTouchExplorationEnabled()94 public boolean isTouchExplorationEnabled() { 95 return ENABLE_ACCESSIBILITY 96 && mAccessibilityManager.isEnabled() 97 && mAccessibilityManager.isTouchExplorationEnabled(); 98 } 99 100 /** 101 * Returns {@true} if the provided event is a touch exploration (e.g. hover) 102 * event. This is used to determine whether the event should be processed by 103 * the touch exploration code within the keyboard. 104 * 105 * @param event The event to check. 106 * @return {@true} is the event is a touch exploration event 107 */ isTouchExplorationEvent(MotionEvent event)108 public boolean isTouchExplorationEvent(MotionEvent event) { 109 final int action = event.getAction(); 110 111 return action == MotionEvent.ACTION_HOVER_ENTER 112 || action == MotionEvent.ACTION_HOVER_EXIT 113 || action == MotionEvent.ACTION_HOVER_MOVE; 114 } 115 116 /** 117 * Returns whether the device should obscure typed password characters. 118 * Typically this means speaking "dot" in place of non-control characters. 119 * 120 * @return {@code true} if the device should obscure password characters. 121 */ shouldObscureInput(EditorInfo editorInfo)122 public boolean shouldObscureInput(EditorInfo editorInfo) { 123 if (editorInfo == null) 124 return false; 125 126 // The user can optionally force speaking passwords. 127 if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) { 128 final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(), 129 SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0; 130 if (speakPassword) 131 return false; 132 } 133 134 // Always speak if the user is listening through headphones. 135 if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) 136 return false; 137 138 // Don't speak if the IME is connected to a password field. 139 return InputTypeUtils.isPasswordInputType(editorInfo.inputType); 140 } 141 142 /** 143 * Sends the specified text to the {@link AccessibilityManager} to be 144 * spoken. 145 * 146 * @param view The source view. 147 * @param text The text to speak. 148 */ announceForAccessibility(View view, CharSequence text)149 public void announceForAccessibility(View view, 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.obtain(); 159 160 event.setPackageName(PACKAGE); 161 event.setClassName(CLASS); 162 event.setEventTime(SystemClock.uptimeMillis()); 163 event.setEnabled(true); 164 event.getText().add(text); 165 166 // Platforms starting at SDK 16 should use announce events. 167 if (Build.VERSION.SDK_INT >= 16) { 168 event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT); 169 } else { 170 event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED); 171 } 172 173 final ViewParent viewParent = view.getParent(); 174 if ((viewParent == null) || !(viewParent instanceof ViewGroup)) { 175 Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility"); 176 return; 177 } 178 179 viewParent.requestSendAccessibilityEvent(view, event); 180 } 181 182 /** 183 * Handles speaking the "connect a headset to hear passwords" notification 184 * when connecting to a password field. 185 * 186 * @param view The source view. 187 * @param editorInfo The input connection's editor info attribute. 188 * @param restarting Whether the connection is being restarted. 189 */ onStartInputViewInternal(View view, EditorInfo editorInfo, boolean restarting)190 public void onStartInputViewInternal(View view, EditorInfo editorInfo, boolean restarting) { 191 if (shouldObscureInput(editorInfo)) { 192 final CharSequence text = mContext.getText(R.string.spoken_use_headphones); 193 announceForAccessibility(view, text); 194 } 195 } 196 197 /** 198 * Sends the specified {@link AccessibilityEvent} if accessibility is 199 * enabled. No operation if accessibility is disabled. 200 * 201 * @param event The event to send. 202 */ requestSendAccessibilityEvent(AccessibilityEvent event)203 public void requestSendAccessibilityEvent(AccessibilityEvent event) { 204 if (mAccessibilityManager.isEnabled()) { 205 mAccessibilityManager.sendAccessibilityEvent(event); 206 } 207 } 208 } 209