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

Commit b3957fbd authored by Jim Miller's avatar Jim Miller Committed by Android Git Automerger
Browse files

am 1035833f: Merge "Improve LockPatternView accessibility" into mnc-dev

* commit '1035833f':
  Improve LockPatternView accessibility
parents a6d4213c 1035833f
Loading
Loading
Loading
Loading
+218 −11
Original line number Diff line number Diff line
@@ -16,31 +16,42 @@

package com.android.internal.widget;


import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Debug;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.IntArray;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;

import com.android.internal.R;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
@@ -80,6 +91,9 @@ public class LockPatternView extends View {
     * It didn't seem to have much impact on the devices tested, so currently set to 0.
     */
    private static final float DRAG_THRESHHOLD = 0.0f;
    public static final int VIRTUAL_BASE_VIEW_ID = 1;
    public static final boolean DEBUG_A11Y = true;
    private static final String TAG = "LockPatternView";

    private OnPatternListener mOnPatternListener;
    private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
@@ -124,6 +138,8 @@ public class LockPatternView extends View {

    private final Interpolator mFastOutSlowInInterpolator;
    private final Interpolator mLinearOutSlowInInterpolator;
    private PatternExploreByTouchHelper mExploreByTouchHelper;
    private AudioManager mAudioManager;

    /**
     * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
@@ -305,6 +321,9 @@ public class LockPatternView extends View {
                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
        mLinearOutSlowInInterpolator =
                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
        mExploreByTouchHelper = new PatternExploreByTouchHelper(this);
        setAccessibilityDelegate(mExploreByTouchHelper);
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
    }

    public CellState[][] getCellStates() {
@@ -394,10 +413,13 @@ public class LockPatternView extends View {
    }

    private void notifyCellAdded() {
        sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
        // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
        if (mOnPatternListener != null) {
            mOnPatternListener.onPatternCellAdded(mPattern);
        }
        // Disable used cells for accessibility as they get added
        if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added.");
        mExploreByTouchHelper.invalidateRoot();
    }

    private void notifyPatternStarted() {
@@ -428,6 +450,13 @@ public class LockPatternView extends View {
        resetPattern();
    }

    @Override
    protected boolean dispatchHoverEvent(MotionEvent event) {
        // Give TouchHelper first right of refusal
        boolean handled = mExploreByTouchHelper.dispatchHoverEvent(event);
        return super.dispatchHoverEvent(event) || handled;
    }

    /**
     * Reset all pattern state.
     */
@@ -469,8 +498,10 @@ public class LockPatternView extends View {
        final int width = w - mPaddingLeft - mPaddingRight;
        mSquareWidth = width / 3.0f;

        if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")");
        final int height = h - mPaddingTop - mPaddingBottom;
        mSquareHeight = height / 3.0f;
        mExploreByTouchHelper.invalidateRoot();
    }

    private int resolveMeasured(int measureSpec, int desired)
@@ -1133,4 +1164,180 @@ public class LockPatternView extends View {
            }
        };
    }

    private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
        private Rect mTempRect = new Rect();
        private HashMap<Integer, VirtualViewContainer> mItems = new HashMap<Integer,
                VirtualViewContainer>();

        class VirtualViewContainer {
            public VirtualViewContainer(CharSequence description) {
                this.description = description;
            }
            CharSequence description;
        };

        public PatternExploreByTouchHelper(View forView) {
            super(forView);
        }

        @Override
        protected int getVirtualViewAt(float x, float y) {
            // This must use the same hit logic for the screen to ensure consistency whether
            // accessibility is on or off.
            int id = getVirtualViewIdForHit(x, y);
            return id;
        }

        @Override
        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
            if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")");
            for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
                if (!mItems.containsKey(i)) {
                    VirtualViewContainer item = new VirtualViewContainer(getTextForVirtualView(i));
                    mItems.put(i, item);
                }
                // Add all views. As views are added to the pattern, we remove them
                // from notification by making them non-clickable below.
                virtualViewIds.add(i);
            }
        }

        @Override
        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
            if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")");
            // Announce this view
            if (mItems.containsKey(virtualViewId)) {
                CharSequence contentDescription = mItems.get(virtualViewId).description;
                event.getText().add(contentDescription);
            }
        }

        @Override
        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
            if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")");

            // Node and event text and content descriptions are usually
            // identical, so we'll use the exact same string as before.
            node.setText(getTextForVirtualView(virtualViewId));
            node.setContentDescription(getTextForVirtualView(virtualViewId));

            if (isClickable(virtualViewId)) {
                // Mark this node of interest by making it clickable.
                node.addAction(AccessibilityAction.ACTION_CLICK);
                node.setClickable(isClickable(virtualViewId));
            }

            // Compute bounds for this object
            final Rect bounds = getBoundsForVirtualView(virtualViewId);
            if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString());
            node.setBoundsInParent(bounds);
        }

        private boolean isClickable(int virtualViewId) {
            // Dots are clickable if they're not part of the current pattern.
            if (virtualViewId != ExploreByTouchHelper.INVALID_ID) {
                int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3;
                int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3;
                return !mPatternDrawLookup[row][col];
            }
            return false;
        }

        @Override
        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
                Bundle arguments) {
            if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId
                    + ", action=" + action);
            switch (action) {
                case AccessibilityNodeInfo.ACTION_CLICK:
                    // Click handling should be consistent with
                    // onTouchEvent(). This ensures that the view works the
                    // same whether accessibility is turned on or off.
                    return onItemClicked(virtualViewId);
                default:
                    if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in "
                            + "onPerformActionForVirtualView(viewId="
                            + virtualViewId + "action=" + action + ")");
            }
            return false;
        }

        boolean onItemClicked(int index) {
            if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")");

            // Since the item's checked state is exposed to accessibility
            // services through its AccessibilityNodeInfo, we need to invalidate
            // the item's virtual view. At some point in the future, the
            // framework will obtain an updated version of the virtual view.
            invalidateVirtualView(index);

            // We need to let the framework know what type of event
            // happened. Accessibility services may use this event to provide
            // appropriate feedback to the user.
            sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);

            return true;
        }

        private Rect getBoundsForVirtualView(int virtualViewId) {
            int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID;
            final Rect bounds = mTempRect;
            final int row = ordinal / 3;
            final int col = ordinal % 3;
            final CellState cell = mCellStates[row][col];
            float centerX = getCenterXForColumn(col);
            float centerY = getCenterYForRow(row);
            float cellheight = mSquareHeight * mHitFactor * 0.5f;
            float cellwidth = mSquareWidth * mHitFactor * 0.5f;
            float translationY = cell.translateY;
            bounds.left = (int) (centerX - cellwidth);
            bounds.right = (int) (centerX + cellwidth);
            bounds.top = (int) (centerY - cellheight);
            bounds.bottom = (int) (centerY + cellheight);
            return bounds;
        }

        private boolean shouldSpeakPassword() {
            final boolean speakPassword = Settings.Secure.getIntForUser(
                    mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0,
                    UserHandle.USER_CURRENT_OR_SELF) != 0;
            final boolean hasHeadphones = mAudioManager != null ?
                    (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
                    : false;
            return speakPassword || hasHeadphones;
        }

        private CharSequence getTextForVirtualView(int virtualViewId) {
            final Resources res = getResources();
            return shouldSpeakPassword() ? res.getString(
                R.string.lockscreen_access_pattern_cell_added_verbose, virtualViewId)
                : res.getString(R.string.lockscreen_access_pattern_cell_added);
        }

        /**
         * Helper method to find which cell a point maps to
         *
         * if there's no hit.
         * @param x touch position x
         * @param y touch position y
         * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
         */
        private int getVirtualViewIdForHit(float x, float y) {
            final int rowHit = getRowHit(y);
            if (rowHit < 0) {
                return ExploreByTouchHelper.INVALID_ID;
            }
            final int columnHit = getColumnHit(x);
            if (columnHit < 0) {
                return ExploreByTouchHelper.INVALID_ID;
            }
            boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
            int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
            int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
            if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
                    + view + "avail =" + dotAvailable);
            return view;
        }
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -1971,6 +1971,10 @@
    <string name="lockscreen_access_pattern_cleared">Pattern cleared</string>
    <!-- Accessibility description sent when user adds a cell to the pattern. [CHAR LIMIT=NONE]  -->
    <string name="lockscreen_access_pattern_cell_added">Cell added</string>
    <!-- Accessibility description sent when user adds a cell to the pattern. Announces the 
    actual cell when headphones are connected [CHAR LIMIT=NONE]  -->
    <string name="lockscreen_access_pattern_cell_added_verbose">
            Cell <xliff:g id="cell_index" example="3">%1$s</xliff:g> added</string>
    <!-- Accessibility description sent when user completes drawing a pattern. [CHAR LIMIT=NONE] -->
    <string name="lockscreen_access_pattern_detected">Pattern completed</string>

+1 −0
Original line number Diff line number Diff line
@@ -705,6 +705,7 @@
  <java-symbol type="string" name="package_updated_device_owner" />
  <java-symbol type="string" name="package_deleted_device_owner" />
  <java-symbol type="string" name="lockscreen_access_pattern_cell_added" />
  <java-symbol type="string" name="lockscreen_access_pattern_cell_added_verbose" />
  <java-symbol type="string" name="lockscreen_access_pattern_cleared" />
  <java-symbol type="string" name="lockscreen_access_pattern_detected" />
  <java-symbol type="string" name="lockscreen_access_pattern_start" />