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