15ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette/*
25ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * Copyright (C) 2011 The Android Open Source Project
35ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette *
45ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * Licensed under the Apache License, Version 2.0 (the "License");
55ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * you may not use this file except in compliance with the License.
65ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * You may obtain a copy of the License at
75ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette *
85ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette *      http://www.apache.org/licenses/LICENSE-2.0
95ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette *
105ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * Unless required by applicable law or agreed to in writing, software
115ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * distributed under the License is distributed on an "AS IS" BASIS,
125ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
135ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * See the License for the specific language governing permissions and
145ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette * limitations under the License.
155ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette */
165ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
175ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverettepackage com.android.inputmethod.accessibility;
185ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
195ac4638f999db4fea8a9e24171dbceb640a10858Alan Viveretteimport android.content.Context;
205ac4638f999db4fea8a9e24171dbceb640a10858Alan Viveretteimport android.inputmethodservice.InputMethodService;
21b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viveretteimport android.media.AudioManager;
225f312c9c1546da9f73d02f911d3365da4ff658fbalanvimport android.os.Build;
235ac4638f999db4fea8a9e24171dbceb640a10858Alan Viveretteimport android.os.SystemClock;
24c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viveretteimport android.provider.Settings;
255f312c9c1546da9f73d02f911d3365da4ff658fbalanvimport android.support.v4.view.accessibility.AccessibilityEventCompat;
265ac4638f999db4fea8a9e24171dbceb640a10858Alan Viveretteimport android.util.Log;
275ac4638f999db4fea8a9e24171dbceb640a10858Alan Viveretteimport android.view.MotionEvent;
285f312c9c1546da9f73d02f911d3365da4ff658fbalanvimport android.view.View;
295f312c9c1546da9f73d02f911d3365da4ff658fbalanvimport android.view.ViewGroup;
305f312c9c1546da9f73d02f911d3365da4ff658fbalanvimport android.view.ViewParent;
315ac4638f999db4fea8a9e24171dbceb640a10858Alan Viveretteimport android.view.accessibility.AccessibilityEvent;
325ac4638f999db4fea8a9e24171dbceb640a10858Alan Viveretteimport android.view.accessibility.AccessibilityManager;
33b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viveretteimport android.view.inputmethod.EditorInfo;
345ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
35b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viveretteimport com.android.inputmethod.compat.AudioManagerCompatWrapper;
36c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viveretteimport com.android.inputmethod.compat.SettingsSecureCompatUtils;
37be55086fd9218bc03ee0ccac1052d96b40d8a979Tadashi G. Takaokaimport com.android.inputmethod.latin.InputTypeUtils;
38b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viveretteimport com.android.inputmethod.latin.R;
395ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
401e11c44d1b5f9ddf593c5407cb14c458be0056f2Tadashi G. Takaokapublic final class AccessibilityUtils {
415ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final String TAG = AccessibilityUtils.class.getSimpleName();
425ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final String CLASS = AccessibilityUtils.class.getClass().getName();
435ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage()
445ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette            .getName();
455ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
465ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final AccessibilityUtils sInstance = new AccessibilityUtils();
475ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
48b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    private Context mContext;
495ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private AccessibilityManager mAccessibilityManager;
50b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    private AudioManagerCompatWrapper mAudioManager;
515ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
525ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    /*
535ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * Setting this constant to {@code false} will disable all keyboard
545ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * accessibility code, regardless of whether Accessibility is turned on in
555ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * the system settings. It should ONLY be used in the event of an emergency.
565ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     */
575ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final boolean ENABLE_ACCESSIBILITY = true;
585ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
592ac5988f84b5c38d313951a3d7faddebf5f25e04Tadashi G. Takaoka    public static void init(InputMethodService inputMethod) {
605ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        if (!ENABLE_ACCESSIBILITY)
615ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette            return;
625ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
635ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // These only need to be initialized if the kill switch is off.
642ac5988f84b5c38d313951a3d7faddebf5f25e04Tadashi G. Takaoka        sInstance.initInternal(inputMethod);
652ac5988f84b5c38d313951a3d7faddebf5f25e04Tadashi G. Takaoka        KeyCodeDescriptionMapper.init();
662ac5988f84b5c38d313951a3d7faddebf5f25e04Tadashi G. Takaoka        AccessibleKeyboardViewProxy.init(inputMethod);
675ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
685ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
695ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    public static AccessibilityUtils getInstance() {
705ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        return sInstance;
715ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
725ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
735ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private AccessibilityUtils() {
745ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // This class is not publicly instantiable.
755ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
765ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
772ac5988f84b5c38d313951a3d7faddebf5f25e04Tadashi G. Takaoka    private void initInternal(Context context) {
78b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        mContext = context;
795ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        mAccessibilityManager = (AccessibilityManager) context
805ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette                .getSystemService(Context.ACCESSIBILITY_SERVICE);
81b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
82b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        final AudioManager audioManager = (AudioManager) context
83b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette                .getSystemService(Context.AUDIO_SERVICE);
84b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        mAudioManager = new AudioManagerCompatWrapper(audioManager);
855ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
865ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
875ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    /**
885ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * Returns {@code true} if touch exploration is enabled. Currently, this
895ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * means that the kill switch is off, the device supports touch exploration,
905ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * and a spoken feedback service is turned on.
915ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     *
925ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * @return {@code true} if touch exploration is enabled.
935ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     */
945ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    public boolean isTouchExplorationEnabled() {
955ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        return ENABLE_ACCESSIBILITY
965ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette                && mAccessibilityManager.isEnabled()
97c6435f92a80c6664870f9d1a4bb2a1c5153ef2c3Tadashi G. Takaoka                && mAccessibilityManager.isTouchExplorationEnabled();
985ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
995ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1005ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    /**
1015ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * Returns {@true} if the provided event is a touch exploration (e.g. hover)
1025ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * event. This is used to determine whether the event should be processed by
1035ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * the touch exploration code within the keyboard.
1045ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     *
1055ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * @param event The event to check.
1065ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * @return {@true} is the event is a touch exploration event
1075ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     */
1085ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    public boolean isTouchExplorationEvent(MotionEvent event) {
1095ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        final int action = event.getAction();
1105ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
111c6435f92a80c6664870f9d1a4bb2a1c5153ef2c3Tadashi G. Takaoka        return action == MotionEvent.ACTION_HOVER_ENTER
112c6435f92a80c6664870f9d1a4bb2a1c5153ef2c3Tadashi G. Takaoka                || action == MotionEvent.ACTION_HOVER_EXIT
113c6435f92a80c6664870f9d1a4bb2a1c5153ef2c3Tadashi G. Takaoka                || action == MotionEvent.ACTION_HOVER_MOVE;
1145ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
1155ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1165ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    /**
117c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette     * Returns whether the device should obscure typed password characters.
118c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette     * Typically this means speaking "dot" in place of non-control characters.
1199a81ce92c381007affe6bb2310bf94c9856eaae1alanv     *
120c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette     * @return {@code true} if the device should obscure password characters.
121b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     */
122e7eac906c0a14b644d457beeb73a407fa1b63673Tadashi G. Takaoka    public boolean shouldObscureInput(EditorInfo editorInfo) {
123e7eac906c0a14b644d457beeb73a407fa1b63673Tadashi G. Takaoka        if (editorInfo == null)
124b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette            return false;
125b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
126c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette        // The user can optionally force speaking passwords.
127c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette        if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) {
128c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette            final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(),
129c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette                    SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
130c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette            if (speakPassword)
131c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette                return false;
132c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette        }
133c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette
134b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        // Always speak if the user is listening through headphones.
135b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
136b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette            return false;
137b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
138b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        // Don't speak if the IME is connected to a password field.
139be55086fd9218bc03ee0ccac1052d96b40d8a979Tadashi G. Takaoka        return InputTypeUtils.isPasswordInputType(editorInfo.inputType);
140b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    }
141b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
142b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    /**
1435ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * Sends the specified text to the {@link AccessibilityManager} to be
1445ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * spoken.
1455ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     *
1465f312c9c1546da9f73d02f911d3365da4ff658fbalanv     * @param view The source view.
1475f312c9c1546da9f73d02f911d3365da4ff658fbalanv     * @param text The text to speak.
1485ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     */
1495f312c9c1546da9f73d02f911d3365da4ff658fbalanv    public void announceForAccessibility(View view, CharSequence text) {
1505ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        if (!mAccessibilityManager.isEnabled()) {
1515ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette            Log.e(TAG, "Attempted to speak when accessibility was disabled!");
1525ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette            return;
1535ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        }
1545ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1555ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // The following is a hack to avoid using the heavy-weight TextToSpeech
1565ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // class. Instead, we're just forcing a fake AccessibilityEvent into
1575ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // the screen reader to make it speak.
1585f312c9c1546da9f73d02f911d3365da4ff658fbalanv        final AccessibilityEvent event = AccessibilityEvent.obtain();
1595ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1605ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.setPackageName(PACKAGE);
1615ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.setClassName(CLASS);
1625ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.setEventTime(SystemClock.uptimeMillis());
1635ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.setEnabled(true);
1645ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.getText().add(text);
1655ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1665f312c9c1546da9f73d02f911d3365da4ff658fbalanv        // Platforms starting at SDK 16 should use announce events.
1675f312c9c1546da9f73d02f911d3365da4ff658fbalanv        if (Build.VERSION.SDK_INT >= 16) {
1685f312c9c1546da9f73d02f911d3365da4ff658fbalanv            event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
1695f312c9c1546da9f73d02f911d3365da4ff658fbalanv        } else {
1705f312c9c1546da9f73d02f911d3365da4ff658fbalanv            event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
1715f312c9c1546da9f73d02f911d3365da4ff658fbalanv        }
1725f312c9c1546da9f73d02f911d3365da4ff658fbalanv
1735f312c9c1546da9f73d02f911d3365da4ff658fbalanv        final ViewParent viewParent = view.getParent();
1745f312c9c1546da9f73d02f911d3365da4ff658fbalanv        if ((viewParent == null) || !(viewParent instanceof ViewGroup)) {
1755f312c9c1546da9f73d02f911d3365da4ff658fbalanv            Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility");
1765f312c9c1546da9f73d02f911d3365da4ff658fbalanv            return;
1775f312c9c1546da9f73d02f911d3365da4ff658fbalanv        }
1785f312c9c1546da9f73d02f911d3365da4ff658fbalanv
1795f312c9c1546da9f73d02f911d3365da4ff658fbalanv        viewParent.requestSendAccessibilityEvent(view, event);
1805ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
181b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
182b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    /**
183b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     * Handles speaking the "connect a headset to hear passwords" notification
184b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     * when connecting to a password field.
185b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     *
1865f312c9c1546da9f73d02f911d3365da4ff658fbalanv     * @param view The source view.
187e7eac906c0a14b644d457beeb73a407fa1b63673Tadashi G. Takaoka     * @param editorInfo The input connection's editor info attribute.
188b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     * @param restarting Whether the connection is being restarted.
189b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     */
1905f312c9c1546da9f73d02f911d3365da4ff658fbalanv    public void onStartInputViewInternal(View view, EditorInfo editorInfo, boolean restarting) {
191e7eac906c0a14b644d457beeb73a407fa1b63673Tadashi G. Takaoka        if (shouldObscureInput(editorInfo)) {
192b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette            final CharSequence text = mContext.getText(R.string.spoken_use_headphones);
1935f312c9c1546da9f73d02f911d3365da4ff658fbalanv            announceForAccessibility(view, text);
194b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        }
195b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    }
196282adf733093b41a31514746825ea05fc90fb3eealanv
197282adf733093b41a31514746825ea05fc90fb3eealanv    /**
198282adf733093b41a31514746825ea05fc90fb3eealanv     * Sends the specified {@link AccessibilityEvent} if accessibility is
199282adf733093b41a31514746825ea05fc90fb3eealanv     * enabled. No operation if accessibility is disabled.
200282adf733093b41a31514746825ea05fc90fb3eealanv     *
201282adf733093b41a31514746825ea05fc90fb3eealanv     * @param event The event to send.
202282adf733093b41a31514746825ea05fc90fb3eealanv     */
203282adf733093b41a31514746825ea05fc90fb3eealanv    public void requestSendAccessibilityEvent(AccessibilityEvent event) {
204282adf733093b41a31514746825ea05fc90fb3eealanv        if (mAccessibilityManager.isEnabled()) {
205282adf733093b41a31514746825ea05fc90fb3eealanv            mAccessibilityManager.sendAccessibilityEvent(event);
206282adf733093b41a31514746825ea05fc90fb3eealanv        }
207282adf733093b41a31514746825ea05fc90fb3eealanv    }
2085ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette}
209