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

Commit 8521781f authored by Svetoslav Ganov's avatar Svetoslav Ganov Committed by Android (Google) Code Review
Browse files

Merge "Added support for touch exploration to Latin IME."

parents ae706548 5ac4638f
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