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
35c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viveretteimport com.android.inputmethod.compat.SettingsSecureCompatUtils;
36be55086fd9218bc03ee0ccac1052d96b40d8a979Tadashi G. Takaokaimport com.android.inputmethod.latin.InputTypeUtils;
37b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viveretteimport com.android.inputmethod.latin.R;
385ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
391e11c44d1b5f9ddf593c5407cb14c458be0056f2Tadashi G. Takaokapublic final class AccessibilityUtils {
405ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final String TAG = AccessibilityUtils.class.getSimpleName();
415ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final String CLASS = AccessibilityUtils.class.getClass().getName();
421e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa    private static final String PACKAGE =
431e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa            AccessibilityUtils.class.getClass().getPackage().getName();
445ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
455ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final AccessibilityUtils sInstance = new AccessibilityUtils();
465ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
47b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    private Context mContext;
485ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private AccessibilityManager mAccessibilityManager;
491e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa    private AudioManager mAudioManager;
505ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
515ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    /*
525ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * Setting this constant to {@code false} will disable all keyboard
535ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * accessibility code, regardless of whether Accessibility is turned on in
545ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * the system settings. It should ONLY be used in the event of an emergency.
555ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     */
565ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private static final boolean ENABLE_ACCESSIBILITY = true;
575ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
58b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa    public static void init(final InputMethodService inputMethod) {
591e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa        if (!ENABLE_ACCESSIBILITY) return;
605ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
615ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // These only need to be initialized if the kill switch is off.
622ac5988f84b5c38d313951a3d7faddebf5f25e04Tadashi G. Takaoka        sInstance.initInternal(inputMethod);
632ac5988f84b5c38d313951a3d7faddebf5f25e04Tadashi G. Takaoka        KeyCodeDescriptionMapper.init();
642ac5988f84b5c38d313951a3d7faddebf5f25e04Tadashi G. Takaoka        AccessibleKeyboardViewProxy.init(inputMethod);
655ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
665ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
675ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    public static AccessibilityUtils getInstance() {
685ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        return sInstance;
695ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
705ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
715ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    private AccessibilityUtils() {
725ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // This class is not publicly instantiable.
735ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
745ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
75b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa    private void initInternal(final Context context) {
76b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        mContext = context;
771e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa        mAccessibilityManager =
781e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa                (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
791e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
805ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
815ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
825ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    /**
83c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette     * Returns {@code true} if accessibility is enabled. Currently, this means
84c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette     * that the kill switch is off and system accessibility is turned on.
85c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette     *
86c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette     * @return {@code true} if accessibility is enabled.
87c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette     */
88c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette    public boolean isAccessibilityEnabled() {
89c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette        return ENABLE_ACCESSIBILITY && mAccessibilityManager.isEnabled();
90c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette    }
91c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette
92c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette    /**
935ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * Returns {@code true} if touch exploration is enabled. Currently, this
945ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * means that the kill switch is off, the device supports touch exploration,
95c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette     * and system accessibility is turned on.
965ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     *
975ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * @return {@code true} if touch exploration is enabled.
985ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     */
995ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    public boolean isTouchExplorationEnabled() {
100c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette        return isAccessibilityEnabled() && mAccessibilityManager.isTouchExplorationEnabled();
1015ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
1025ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1035ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    /**
1045ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * Returns {@true} if the provided event is a touch exploration (e.g. hover)
1055ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * event. This is used to determine whether the event should be processed by
1065ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * the touch exploration code within the keyboard.
1075ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     *
1085ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * @param event The event to check.
1095ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * @return {@true} is the event is a touch exploration event
1105ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     */
111b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa    public boolean isTouchExplorationEvent(final MotionEvent event) {
1125ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        final int action = event.getAction();
113c6435f92a80c6664870f9d1a4bb2a1c5153ef2c3Tadashi G. Takaoka        return action == MotionEvent.ACTION_HOVER_ENTER
114c6435f92a80c6664870f9d1a4bb2a1c5153ef2c3Tadashi G. Takaoka                || action == MotionEvent.ACTION_HOVER_EXIT
115c6435f92a80c6664870f9d1a4bb2a1c5153ef2c3Tadashi G. Takaoka                || action == MotionEvent.ACTION_HOVER_MOVE;
1165ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
1175ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1185ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    /**
119c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette     * Returns whether the device should obscure typed password characters.
120c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette     * Typically this means speaking "dot" in place of non-control characters.
1219a81ce92c381007affe6bb2310bf94c9856eaae1alanv     *
122c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette     * @return {@code true} if the device should obscure password characters.
123b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     */
124c2ee72a214fef46bc02ce486220365bbefd78714Alan Viverette    @SuppressWarnings("deprecation")
125b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa    public boolean shouldObscureInput(final EditorInfo editorInfo) {
1261e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa        if (editorInfo == null) return false;
127b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
128c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette        // The user can optionally force speaking passwords.
129c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette        if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) {
130c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette            final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(),
131c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette                    SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
1321e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa            if (speakPassword) return false;
133c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette        }
134c960695f38ae0564dff3a6897fd1843c8e74c604Alan Viverette
135b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        // Always speak if the user is listening through headphones.
1361e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa        if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) {
137b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette            return false;
1381e6edb3e5728f82d45bc2677fd72aa654b37ee73Ken Wakasa        }
139b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
140b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        // Don't speak if the IME is connected to a password field.
141be55086fd9218bc03ee0ccac1052d96b40d8a979Tadashi G. Takaoka        return InputTypeUtils.isPasswordInputType(editorInfo.inputType);
142b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    }
143b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
144b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    /**
1455ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * Sends the specified text to the {@link AccessibilityManager} to be
1465ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     * spoken.
1475ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     *
1485f312c9c1546da9f73d02f911d3365da4ff658fbalanv     * @param view The source view.
1495f312c9c1546da9f73d02f911d3365da4ff658fbalanv     * @param text The text to speak.
1505ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette     */
151b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa    public void announceForAccessibility(final View view, final CharSequence text) {
1525ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        if (!mAccessibilityManager.isEnabled()) {
1535ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette            Log.e(TAG, "Attempted to speak when accessibility was disabled!");
1545ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette            return;
1555ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        }
1565ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1575ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // The following is a hack to avoid using the heavy-weight TextToSpeech
1585ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // class. Instead, we're just forcing a fake AccessibilityEvent into
1595ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        // the screen reader to make it speak.
1605f312c9c1546da9f73d02f911d3365da4ff658fbalanv        final AccessibilityEvent event = AccessibilityEvent.obtain();
1615ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
1625ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.setPackageName(PACKAGE);
1635ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.setClassName(CLASS);
1645ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.setEventTime(SystemClock.uptimeMillis());
1655ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.setEnabled(true);
1665ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette        event.getText().add(text);
1675ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette
168b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa        // Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use
169b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa        // announce events.
170b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1715f312c9c1546da9f73d02f911d3365da4ff658fbalanv            event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
1725f312c9c1546da9f73d02f911d3365da4ff658fbalanv        } else {
1735f312c9c1546da9f73d02f911d3365da4ff658fbalanv            event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
1745f312c9c1546da9f73d02f911d3365da4ff658fbalanv        }
1755f312c9c1546da9f73d02f911d3365da4ff658fbalanv
1765f312c9c1546da9f73d02f911d3365da4ff658fbalanv        final ViewParent viewParent = view.getParent();
1775f312c9c1546da9f73d02f911d3365da4ff658fbalanv        if ((viewParent == null) || !(viewParent instanceof ViewGroup)) {
1785f312c9c1546da9f73d02f911d3365da4ff658fbalanv            Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility");
1795f312c9c1546da9f73d02f911d3365da4ff658fbalanv            return;
1805f312c9c1546da9f73d02f911d3365da4ff658fbalanv        }
1815f312c9c1546da9f73d02f911d3365da4ff658fbalanv
1825f312c9c1546da9f73d02f911d3365da4ff658fbalanv        viewParent.requestSendAccessibilityEvent(view, event);
1835ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette    }
184b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette
185b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    /**
186b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     * Handles speaking the "connect a headset to hear passwords" notification
187b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     * when connecting to a password field.
188b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     *
1895f312c9c1546da9f73d02f911d3365da4ff658fbalanv     * @param view The source view.
190e7eac906c0a14b644d457beeb73a407fa1b63673Tadashi G. Takaoka     * @param editorInfo The input connection's editor info attribute.
191b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     * @param restarting Whether the connection is being restarted.
192b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette     */
193b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa    public void onStartInputViewInternal(final View view, final EditorInfo editorInfo,
194b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa            final boolean restarting) {
195e7eac906c0a14b644d457beeb73a407fa1b63673Tadashi G. Takaoka        if (shouldObscureInput(editorInfo)) {
196b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette            final CharSequence text = mContext.getText(R.string.spoken_use_headphones);
1975f312c9c1546da9f73d02f911d3365da4ff658fbalanv            announceForAccessibility(view, text);
198b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette        }
199b0c8db018d53b103dcb4b699be27a4e1a2c2f92cAlan Viverette    }
200282adf733093b41a31514746825ea05fc90fb3eealanv
201282adf733093b41a31514746825ea05fc90fb3eealanv    /**
202282adf733093b41a31514746825ea05fc90fb3eealanv     * Sends the specified {@link AccessibilityEvent} if accessibility is
203282adf733093b41a31514746825ea05fc90fb3eealanv     * enabled. No operation if accessibility is disabled.
204282adf733093b41a31514746825ea05fc90fb3eealanv     *
205282adf733093b41a31514746825ea05fc90fb3eealanv     * @param event The event to send.
206282adf733093b41a31514746825ea05fc90fb3eealanv     */
207b6ca354431367b625daf9fff5fbe4b1f5ef996abKen Wakasa    public void requestSendAccessibilityEvent(final AccessibilityEvent event) {
208282adf733093b41a31514746825ea05fc90fb3eealanv        if (mAccessibilityManager.isEnabled()) {
209282adf733093b41a31514746825ea05fc90fb3eealanv            mAccessibilityManager.sendAccessibilityEvent(event);
210282adf733093b41a31514746825ea05fc90fb3eealanv        }
211282adf733093b41a31514746825ea05fc90fb3eealanv    }
2125ac4638f999db4fea8a9e24171dbceb640a10858Alan Viverette}
213