Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5ac4638f authored by Alan Viverette's avatar Alan Viverette
Browse files

Added support for touch exploration to Latin IME.

Bug: 4379983
Change-Id: I97f22e54827c6229054b514801401ffa5b4ed3b8
parent 3edc97b2
Loading
Loading
Loading
Loading
+75 −0
Original line number Diff line number Diff line
@@ -131,6 +131,81 @@
    <!-- Label for "Wait" key of phone number keyboard.  Must be short to fit on key! [CHAR LIMIT=5]-->
    <string name="label_wait_key">Wait</string>

    <!-- Spoken description for the currently entered text -->
    <string name="spoken_current_text_is">Current text is "%s"</string>
    <!-- Spoken description when there is no text entered -->
    <string name="spoken_no_text_entered">No text entered</string>

    <!-- Spoken description for unknown keyboard keys. -->
    <string name="spoken_description_unknown">Key code %d</string>
    <!-- Spoken description for the "Shift" keyboard key. -->
    <string name="spoken_description_shift">Shift</string>
    <!-- Spoken description for the "Shift" keyboard key's pressed state. -->
    <string name="spoken_description_shift_shifted">Shift enabled</string>
    <!-- Spoken description for the "Shift" keyboard key's pressed state. -->
    <string name="spoken_description_caps_lock">Caps lock enabled</string>
    <!-- Spoken description for the "Delete" keyboard key. -->
    <string name="spoken_description_delete">Delete</string>
    <!-- Spoken description for the "To Symbol" keyboard key. -->
    <string name="spoken_description_to_symbol">Symbols</string>
    <!-- Spoken description for the "To Alpha" keyboard key. -->
    <string name="spoken_description_to_alpha">Letters</string>
    <!-- Spoken description for the "To Numbers" keyboard key. -->
    <string name="spoken_description_to_numeric">Numbers</string>
    <!-- Spoken description for the "Settings" keyboard key. -->
    <string name="spoken_description_settings">Settings</string>
    <!-- Spoken description for the "Tab" keyboard key. -->
    <string name="spoken_description_tab">Tab</string>
    <!-- Spoken description for the "Space" keyboard key. -->
    <string name="spoken_description_space">Space</string>
    <!-- Spoken description for the "Mic" keyboard key. -->
    <string name="spoken_description_mic">Voice input</string>
    <!-- Spoken description for the "Smiley" keyboard key. -->
    <string name="spoken_description_smiley">Smiley face</string>
    <!-- Spoken description for the "Return" keyboard key. -->
    <string name="spoken_description_return">Return</string>

    <!-- Spoken description for the "," keyboard key. -->
    <string name="spoken_description_comma">Comma</string>
    <!-- Spoken description for the "." keyboard key. -->
    <string name="spoken_description_period">Period</string>
    <!-- Spoken description for the "(" keyboard key. -->
    <string name="spoken_description_left_parenthesis">Left parenthesis</string>
    <!-- Spoken description for the ")" keyboard key. -->
    <string name="spoken_description_right_parenthesis">Right parenthesis</string>
    <!-- Spoken description for the ":" keyboard key. -->
    <string name="spoken_description_colon">Colon</string>
    <!-- Spoken description for the ";" keyboard key. -->
    <string name="spoken_description_semicolon">Semicolon</string>
    <!-- Spoken description for the "!" keyboard key. -->
    <string name="spoken_description_exclamation_mark">Exclamation mark</string>
    <!-- Spoken description for the "?" keyboard key. -->
    <string name="spoken_description_question_mark">Question mark</string>
    <!-- Spoken description for the """ keyboard key. -->
    <string name="spoken_description_double_quote">Double quote</string>
    <!-- Spoken description for the "'" keyboard key. -->
    <string name="spoken_description_single_quote">Single quote</string>
    <!-- Spoken description for the "•" keyboard key. -->
    <string name="spoken_description_dot">Dot</string>
    <!-- Spoken description for the "√" keyboard key. -->
    <string name="spoken_description_square_root">Square root</string>
    <!-- Spoken description for the "π" keyboard key. -->
    <string name="spoken_description_pi">Pi</string>
    <!-- Spoken description for the "Δ" keyboard key. -->
    <string name="spoken_description_delta">Delta</string>
    <!-- Spoken description for the "™" keyboard key. -->
    <string name="spoken_description_trademark">Trademark</string>
    <!-- Spoken description for the "℅" keyboard key. -->
    <string name="spoken_description_care_of">Care of</string>
    <!-- Spoken description for the "*" keyboard key. -->
    <string name="spoken_description_star">Star</string>
    <!-- Spoken description for the "#" keyboard key. -->
    <string name="spoken_description_pound">Pound</string>
    <!-- Spoken description for the "…" keyboard key. -->
    <string name="spoken_description_ellipsis">Ellipsis</string>
    <!-- Spoken description for the "„" keyboard key. -->
    <string name="spoken_description_low_double_quote">Low double quote</string>

    <!-- Voice related labels -->

    <!-- Title of the warning dialog that shows when a user initiates voice input for
+133 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.inputmethod.accessibility;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;

import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
import com.android.inputmethod.compat.AccessibilityManagerCompatWrapper;
import com.android.inputmethod.compat.MotionEventCompatUtils;

public class AccessibilityUtils {
    private static final String TAG = AccessibilityUtils.class.getSimpleName();
    private static final String CLASS = AccessibilityUtils.class.getClass().getName();
    private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage()
            .getName();

    private static final AccessibilityUtils sInstance = new AccessibilityUtils();

    private AccessibilityManager mAccessibilityManager;
    private AccessibilityManagerCompatWrapper mCompatManager;

    /*
     * Setting this constant to {@code false} will disable all keyboard
     * accessibility code, regardless of whether Accessibility is turned on in
     * the system settings. It should ONLY be used in the event of an emergency.
     */
    private static final boolean ENABLE_ACCESSIBILITY = true;

    public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
        if (!ENABLE_ACCESSIBILITY)
            return;

        // These only need to be initialized if the kill switch is off.
        sInstance.initInternal(inputMethod, prefs);
        KeyCodeDescriptionMapper.init(inputMethod, prefs);
        AccessibleInputMethodServiceProxy.init(inputMethod, prefs);
        AccessibleKeyboardViewProxy.init(inputMethod, prefs);
    }

    public static AccessibilityUtils getInstance() {
        return sInstance;
    }

    private AccessibilityUtils() {
        // This class is not publicly instantiable.
    }

    private void initInternal(Context context, SharedPreferences prefs) {
        mAccessibilityManager = (AccessibilityManager) context
                .getSystemService(Context.ACCESSIBILITY_SERVICE);
        mCompatManager = new AccessibilityManagerCompatWrapper(mAccessibilityManager);
    }

    /**
     * Returns {@code true} if touch exploration is enabled. Currently, this
     * means that the kill switch is off, the device supports touch exploration,
     * and a spoken feedback service is turned on.
     *
     * @return {@code true} if touch exploration is enabled.
     */
    public boolean isTouchExplorationEnabled() {
        return ENABLE_ACCESSIBILITY
                && AccessibilityEventCompatUtils.supportsTouchExploration()
                && mAccessibilityManager.isEnabled()
                && !mCompatManager.getEnabledAccessibilityServiceList(
                        AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty();
    }

    /**
     * Returns {@true} if the provided event is a touch exploration (e.g. hover)
     * event. This is used to determine whether the event should be processed by
     * the touch exploration code within the keyboard.
     *
     * @param event The event to check.
     * @return {@true} is the event is a touch exploration event
     */
    public boolean isTouchExplorationEvent(MotionEvent event) {
        final int action = event.getAction();

        return action == MotionEventCompatUtils.ACTION_HOVER_ENTER
                || action == MotionEventCompatUtils.ACTION_HOVER_EXIT
                || action == MotionEventCompatUtils.ACTION_HOVER_MOVE;
    }

    /**
     * Sends the specified text to the {@link AccessibilityManager} to be
     * spoken.
     *
     * @param text the text to speak
     */
    public void speak(CharSequence text) {
        if (!mAccessibilityManager.isEnabled()) {
            Log.e(TAG, "Attempted to speak when accessibility was disabled!");
            return;
        }

        // The following is a hack to avoid using the heavy-weight TextToSpeech
        // class. Instead, we're just forcing a fake AccessibilityEvent into
        // the screen reader to make it speak.
        final AccessibilityEvent event = AccessibilityEvent
                .obtain(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER);

        event.setPackageName(PACKAGE);
        event.setClassName(CLASS);
        event.setEventTime(SystemClock.uptimeMillis());
        event.setEnabled(true);
        event.getText().add(text);

        mAccessibilityManager.sendAccessibilityEvent(event);
    }
}
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.inputmethod.accessibility;

import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;

import com.android.inputmethod.latin.R;

public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActionListener {
    private static final AccessibleInputMethodServiceProxy sInstance =
            new AccessibleInputMethodServiceProxy();

    /*
     * Delay for the handler event that's fired when Accessibility is on and the
     * user hovers outside of any valid keys. This is used to let the user know
     * that if they lift their finger, nothing will be typed.
     */
    private static final long DELAY_NO_HOVER_SELECTION = 250;

    private InputMethodService mInputMethod;

    private AccessibilityHandler mAccessibilityHandler;

    private class AccessibilityHandler extends Handler {
        private static final int MSG_NO_HOVER_SELECTION = 0;

        public AccessibilityHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_NO_HOVER_SELECTION:
                notifyNoHoverSelection();
                break;
            }
        }

        public void postNoHoverSelection() {
            removeMessages(MSG_NO_HOVER_SELECTION);
            sendEmptyMessageDelayed(MSG_NO_HOVER_SELECTION, DELAY_NO_HOVER_SELECTION);
        }

        public void cancelNoHoverSelection() {
            removeMessages(MSG_NO_HOVER_SELECTION);
        }
    }

    public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
        sInstance.initInternal(inputMethod, prefs);
    }

    public static AccessibleInputMethodServiceProxy getInstance() {
        return sInstance;
    }

    private AccessibleInputMethodServiceProxy() {
        // Not publicly instantiable.
    }

    private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) {
        mInputMethod = inputMethod;
        mAccessibilityHandler = new AccessibilityHandler(inputMethod.getMainLooper());
    }

    /**
     * If touch exploration is enabled, cancels the event sent by
     * {@link AccessibleInputMethodServiceProxy#onHoverExit(int)} because the
     * user is currently hovering above a key.
     */
    @Override
    public void onHoverEnter(int primaryCode) {
        mAccessibilityHandler.cancelNoHoverSelection();
    }

    /**
     * If touch exploration is enabled, sends a delayed event to notify the user
     * that they are not currently hovering above a key.
     */
    @Override
    public void onHoverExit(int primaryCode) {
        mAccessibilityHandler.postNoHoverSelection();
    }

    /**
     * When Accessibility is turned on, notifies the user that they are not
     * currently hovering above a key. By default this will speak the currently
     * entered text.
     */
    private void notifyNoHoverSelection() {
        final ExtractedText extracted = mInputMethod.getCurrentInputConnection().getExtractedText(
                new ExtractedTextRequest(), 0);

        if (extracted == null)
            return;

        final CharSequence text;

        if (TextUtils.isEmpty(extracted.text)) {
            text = mInputMethod.getString(R.string.spoken_no_text_entered);
        } else {
            text = mInputMethod.getString(R.string.spoken_current_text_is, extracted.text);
        }

        AccessibilityUtils.getInstance().speak(text);
    }
}
+37 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.inputmethod.accessibility;

public interface AccessibleKeyboardActionListener {
    /**
     * Called when the user hovers inside a key. This is sent only when
     * Accessibility is turned on. For keys that repeat, this is only called
     * once.
     *
     * @param primaryCode the code of the key that was hovered over
     */
    public void onHoverEnter(int primaryCode);

    /**
     * Called when the user hovers outside a key. This is sent only when
     * Accessibility is turned on. For keys that repeat, this is only called
     * once.
     *
     * @param primaryCode the code of the key that was hovered over
     */
    public void onHoverExit(int primaryCode);
}
+201 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.android.inputmethod.accessibility;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;

import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
import com.android.inputmethod.compat.MotionEventCompatUtils;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.keyboard.PointerTracker;

public class AccessibleKeyboardViewProxy {
    private static final String TAG = AccessibleKeyboardViewProxy.class.getSimpleName();
    private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy();

    // Delay in milliseconds between key press DOWN and UP events
    private static final long DELAY_KEY_PRESS = 10;

    private int mScaledEdgeSlop;
    private KeyboardView mView;
    private AccessibleKeyboardActionListener mListener;

    private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
    private int mLastX = -1;
    private int mLastY = -1;

    public static void init(Context context, SharedPreferences prefs) {
        sInstance.initInternal(context, prefs);
        sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance();
    }

    public static AccessibleKeyboardViewProxy getInstance() {
        return sInstance;
    }

    public static void setView(KeyboardView view) {
        sInstance.mView = view;
    }

    private AccessibleKeyboardViewProxy() {
        // Not publicly instantiable.
    }

    private void initInternal(Context context, SharedPreferences prefs) {
        final Paint paint = new Paint();
        paint.setTextAlign(Paint.Align.LEFT);
        paint.setTextSize(14.0f);
        paint.setAntiAlias(true);
        paint.setColor(Color.YELLOW);

        mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
    }

    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event,
            PointerTracker tracker) {
        if (mView == null) {
            Log.e(TAG, "No keyboard view set!");
            return false;
        }

        switch (event.getEventType()) {
        case AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER:
            final Key key = tracker.getKey(mLastHoverKeyIndex);

            if (key == null)
                break;

            final CharSequence description = KeyCodeDescriptionMapper.getInstance()
                    .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key);

            if (description == null)
                return false;

            event.getText().add(description);

            break;
        }

        return true;
    }

    /**
     * Receives hover events when accessibility is turned on in API > 11. In
     * earlier API levels, events are manually routed from onTouchEvent.
     *
     * @param event The hover event.
     * @return {@code true} if the event is handled
     */
    public boolean onHoverEvent(MotionEvent event, PointerTracker tracker) {
        return onTouchExplorationEvent(event, tracker);
    }

    public boolean dispatchTouchEvent(MotionEvent event) {
        // Since touch exploration translates hover double-tap to a regular
        // single-tap, we're going to drop non-touch exploration events.
        if (!AccessibilityUtils.getInstance().isTouchExplorationEvent(event))
            return true;

        return false;
    }

    /**
     * Handles touch exploration events when Accessibility is turned on.
     *
     * @param event The touch exploration hover event.
     * @return {@code true} if the event was handled
     */
    private boolean onTouchExplorationEvent(MotionEvent event, PointerTracker tracker) {
        final int x = (int) event.getX();
        final int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEventCompatUtils.ACTION_HOVER_ENTER:
        case MotionEventCompatUtils.ACTION_HOVER_MOVE:
            final int keyIndex = tracker.getKeyIndexOn(x, y);

            if (keyIndex != mLastHoverKeyIndex) {
                fireKeyHoverEvent(tracker, mLastHoverKeyIndex, false);
                mLastHoverKeyIndex = keyIndex;
                mLastX = x;
                mLastY = y;
                fireKeyHoverEvent(tracker, mLastHoverKeyIndex, true);
            }

            return true;
        case MotionEventCompatUtils.ACTION_HOVER_EXIT:
            final int width = mView.getWidth();
            final int height = mView.getHeight();

            if (x < mScaledEdgeSlop || y < mScaledEdgeSlop || x >= (width - mScaledEdgeSlop)
                    || y >= (height - mScaledEdgeSlop)) {
                fireKeyHoverEvent(tracker, mLastHoverKeyIndex, false);
                mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
                mLastX = -1;
                mLastY = -1;
            } else if (mLastHoverKeyIndex != KeyDetector.NOT_A_KEY) {
                fireKeyPressEvent(tracker, mLastX, mLastY, event.getEventTime());
            }

            return true;
        }

        return false;
    }

    private void fireKeyHoverEvent(PointerTracker tracker, int keyIndex, boolean entering) {
        if (mListener == null) {
            Log.e(TAG, "No accessible keyboard action listener set!");
            return;
        }

        if (mView == null) {
            Log.e(TAG, "No keyboard view set!");
            return;
        }

        if (keyIndex == KeyDetector.NOT_A_KEY)
            return;

        final Key key = tracker.getKey(keyIndex);

        if (key == null)
            return;

        if (entering) {
            mListener.onHoverEnter(key.mCode);
            mView.sendAccessibilityEvent(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER);
        } else {
            mListener.onHoverExit(key.mCode);
            mView.sendAccessibilityEvent(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_EXIT);
        }
    }

    private void fireKeyPressEvent(PointerTracker tracker, int x, int y, long eventTime) {
        tracker.onDownEvent(x, y, eventTime, null);
        tracker.onUpEvent(x, y, eventTime + DELAY_KEY_PRESS, null);
    }
}
Loading