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.SystemClock; 23 import android.provider.Settings; 24 import android.util.Log; 25 import android.view.MotionEvent; 26 import android.view.accessibility.AccessibilityEvent; 27 import android.view.accessibility.AccessibilityManager; 28 import android.view.inputmethod.EditorInfo; 29 30 import com.android.inputmethod.compat.AudioManagerCompatWrapper; 31 import com.android.inputmethod.compat.SettingsSecureCompatUtils; 32 import com.android.inputmethod.latin.InputTypeUtils; 33 import com.android.inputmethod.latin.R; 34 35 public class AccessibilityUtils { 36 private static final String TAG = AccessibilityUtils.class.getSimpleName(); 37 private static final String CLASS = AccessibilityUtils.class.getClass().getName(); 38 private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage() 39 .getName(); 40 41 private static final AccessibilityUtils sInstance = new AccessibilityUtils(); 42 43 private Context mContext; 44 private AccessibilityManager mAccessibilityManager; 45 private AudioManagerCompatWrapper mAudioManager; 46 47 /* 48 * Setting this constant to {@code false} will disable all keyboard 49 * accessibility code, regardless of whether Accessibility is turned on in 50 * the system settings. It should ONLY be used in the event of an emergency. 51 */ 52 private static final boolean ENABLE_ACCESSIBILITY = true; 53 54 public static void init(InputMethodService inputMethod) { 55 if (!ENABLE_ACCESSIBILITY) 56 return; 57 58 // These only need to be initialized if the kill switch is off. 59 sInstance.initInternal(inputMethod); 60 KeyCodeDescriptionMapper.init(); 61 AccessibleKeyboardViewProxy.init(inputMethod); 62 } 63 64 public static AccessibilityUtils getInstance() { 65 return sInstance; 66 } 67 68 private AccessibilityUtils() { 69 // This class is not publicly instantiable. 70 } 71 72 private void initInternal(Context context) { 73 mContext = context; 74 mAccessibilityManager = (AccessibilityManager) context 75 .getSystemService(Context.ACCESSIBILITY_SERVICE); 76 77 final AudioManager audioManager = (AudioManager) context 78 .getSystemService(Context.AUDIO_SERVICE); 79 mAudioManager = new AudioManagerCompatWrapper(audioManager); 80 } 81 82 /** 83 * Returns {@code true} if touch exploration is enabled. Currently, this 84 * means that the kill switch is off, the device supports touch exploration, 85 * and a spoken feedback service is turned on. 86 * 87 * @return {@code true} if touch exploration is enabled. 88 */ 89 public boolean isTouchExplorationEnabled() { 90 return ENABLE_ACCESSIBILITY 91 && mAccessibilityManager.isEnabled() 92 && mAccessibilityManager.isTouchExplorationEnabled(); 93 } 94 95 /** 96 * Returns {@true} if the provided event is a touch exploration (e.g. hover) 97 * event. This is used to determine whether the event should be processed by 98 * the touch exploration code within the keyboard. 99 * 100 * @param event The event to check. 101 * @return {@true} is the event is a touch exploration event 102 */ 103 public boolean isTouchExplorationEvent(MotionEvent event) { 104 final int action = event.getAction(); 105 106 return action == MotionEvent.ACTION_HOVER_ENTER 107 || action == MotionEvent.ACTION_HOVER_EXIT 108 || action == MotionEvent.ACTION_HOVER_MOVE; 109 } 110 111 /** 112 * Returns whether the device should obscure typed password characters. 113 * Typically this means speaking "dot" in place of non-control characters. 114 * 115 * @return {@code true} if the device should obscure password characters. 116 */ 117 public boolean shouldObscureInput(EditorInfo editorInfo) { 118 if (editorInfo == null) 119 return false; 120 121 // The user can optionally force speaking passwords. 122 if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) { 123 final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(), 124 SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0; 125 if (speakPassword) 126 return false; 127 } 128 129 // Always speak if the user is listening through headphones. 130 if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) 131 return false; 132 133 // Don't speak if the IME is connected to a password field. 134 return InputTypeUtils.isPasswordInputType(editorInfo.inputType); 135 } 136 137 /** 138 * Sends the specified text to the {@link AccessibilityManager} to be 139 * spoken. 140 * 141 * @param text the text to speak 142 */ 143 public void speak(CharSequence text) { 144 if (!mAccessibilityManager.isEnabled()) { 145 Log.e(TAG, "Attempted to speak when accessibility was disabled!"); 146 return; 147 } 148 149 // The following is a hack to avoid using the heavy-weight TextToSpeech 150 // class. Instead, we're just forcing a fake AccessibilityEvent into 151 // the screen reader to make it speak. 152 final AccessibilityEvent event = AccessibilityEvent 153 .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 154 155 event.setPackageName(PACKAGE); 156 event.setClassName(CLASS); 157 event.setEventTime(SystemClock.uptimeMillis()); 158 event.setEnabled(true); 159 event.getText().add(text); 160 161 mAccessibilityManager.sendAccessibilityEvent(event); 162 } 163 164 /** 165 * Handles speaking the "connect a headset to hear passwords" notification 166 * when connecting to a password field. 167 * 168 * @param editorInfo The input connection's editor info attribute. 169 * @param restarting Whether the connection is being restarted. 170 */ 171 public void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) { 172 if (shouldObscureInput(editorInfo)) { 173 final CharSequence text = mContext.getText(R.string.spoken_use_headphones); 174 speak(text); 175 } 176 } 177 178 /** 179 * Sends the specified {@link AccessibilityEvent} if accessibility is 180 * enabled. No operation if accessibility is disabled. 181 * 182 * @param event The event to send. 183 */ 184 public void requestSendAccessibilityEvent(AccessibilityEvent event) { 185 if (mAccessibilityManager.isEnabled()) { 186 mAccessibilityManager.sendAccessibilityEvent(event); 187 } 188 } 189 } 190