com.android.switchaccess.OptionManager.java Source code

Java tutorial

Introduction

Here is the source code for com.android.switchaccess.OptionManager.java

Source

/*
 * Copyright (C) 2015 Google Inc.
 *
 * 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.switchaccess;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.DisplayMetrics;
import android.util.TypedValue;

import com.android.talkback.R;
import com.android.utils.SharedPreferencesUtils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Manages options in a tree of {@code OptionScanNodes} and traverses them as options are
 * selected.
 */
public class OptionManager implements SharedPreferences.OnSharedPreferenceChangeListener {
    public static final int OPTION_INDEX_CLICK = 0;
    public static final int OPTION_INDEX_NEXT = 1;

    private final OverlayController mOverlayController;

    private final List<OptionManagerListener> mOptionManagerListeners = new ArrayList<>();

    /* TODO Clean up managing the styling information */
    private final Paint[] mOptionPaintArray;
    private final String[] mHighlightColorPrefKeys;
    private final String[] mHighlightColorDefaults;
    private final String[] mHighlightWeightPrefKeys;

    private OptionScanNode mRootNode = null;

    private OptionScanNode mCurrentNode = null;

    private boolean mOptionScanningEnabled = false;
    private ScanListener mScanListener;
    private boolean mStartScanAutomatically = false;

    /**
     * @param overlayController The controller for the overlay on which to present options
     */
    public OptionManager(OverlayController overlayController) {
        mOverlayController = overlayController;
        Context context = mOverlayController.getContext();
        SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(context);
        mHighlightColorPrefKeys = context.getResources()
                .getStringArray(R.array.switch_access_highlight_color_pref_keys);
        mHighlightColorDefaults = context.getResources()
                .getStringArray(R.array.switch_access_highlight_color_defaults);
        mHighlightWeightPrefKeys = context.getResources()
                .getStringArray(R.array.switch_access_highlight_weight_pref_keys);
        mOptionPaintArray = new Paint[mHighlightColorPrefKeys.length];
        for (int i = 0; i < mOptionPaintArray.length; i++) {
            mOptionPaintArray[i] = new Paint();
            mOptionPaintArray[i].setStyle(Paint.Style.STROKE);
        }
        onSharedPreferenceChanged(prefs, null);
        prefs.registerOnSharedPreferenceChangeListener(this);
    }

    /**
     * Clean up when this object is no longer needed
     */
    public void shutdown() {
        SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(mOverlayController.getContext());
        prefs.unregisterOnSharedPreferenceChangeListener(this);
        if (mRootNode != null) {
            mRootNode.recycle();
        }
        mRootNode = null;
    }

    /**
     * Clear any traversal in progress and use the new tree for future traversals
     * @param newTreeRoot The root of the tree to traverse next
     */
    public void clearFocusIfNewTree(OptionScanNode newTreeRoot) {
        if (mRootNode == newTreeRoot) {
            return;
        }
        if (newTreeRoot != null && newTreeRoot.equals(mRootNode)) {
            newTreeRoot.recycle();
            return;
        }
        // new tree is different
        clearFocus();
        if (mRootNode != null) {
            mRootNode.recycle();
        }
        mRootNode = newTreeRoot;
        if (mStartScanAutomatically) {
            selectOption(0);
            for (int i = 0; i < mOptionManagerListeners.size(); i++) {
                OptionManagerListener listener = mOptionManagerListeners.get(i);
                listener.onOptionManagerStartedAutoScan();
            }
        }
    }

    /**
     * Traverse to the child node of the current node that has the specified index and take
     * whatever action is appropriate for that node. If nothing currently has focus, any
     * option moves to the root of the tree.
     * @param optionIndex The index of the child to traverse to. Out-of-bounds indices, such as
     * negative values or those above the index of the last child, cause focus to be reset.
     */
    public void selectOption(int optionIndex) {
        if (optionIndex < 0) {
            clearFocus();
            return;
        }

        /* Move to desired node */
        if (mCurrentNode == null) {
            if (mScanListener != null) {
                mScanListener.onScanStart();
            }
            mCurrentNode = mRootNode;
        } else {
            if (!(mCurrentNode instanceof OptionScanSelectionNode)) {
                /* This should never happen */
                clearFocus();
                return;
            }
            OptionScanSelectionNode selectionNode = (OptionScanSelectionNode) mCurrentNode;
            if (optionIndex >= selectionNode.getChildCount()) {
                // User pressed an option-scan switch for an index greater than this node's order
                if (mScanListener != null) {
                    mScanListener.onScanCompletedWithNoSelection();
                }
                clearFocus();
                return;
            }
            mCurrentNode = selectionNode.getChild(optionIndex);
        }

        onNodeFocused();
    }

    /**
     * Move up the tree to the parent of the current node.
     * @param wrap Controls wrapping when the parent is null. If {@code false}, the current node
     * will not change if the parent is null. If {@code true}, a node from the bottom of the
     * tree will be used instead of a null parent. The bottom node is chosen as the last
     * OptionScanSelectionNode found by repeatedly selecting {@code OPTION_INDEX_NEXT}. Note that
     * this result makes sense for most traditional scanning methods, but may not make perfect
     * sense for all trees.
     */
    public void moveToParent(boolean wrap) {
        if (mCurrentNode != null) {
            mCurrentNode = mCurrentNode.getParent();
            if (mCurrentNode == null) {
                clearFocus();
            } else {
                onNodeFocused();
            }
            return;
        } else if (!wrap) {
            return;
        }

        mCurrentNode = findLastSelectionNode();
        if (mCurrentNode == null) {
            clearFocus();
        } else {
            onNodeFocused();
        }
    }

    /**
     * Register a listener to be notified when focus is cleared
     * @param optionManagerListener A listener that should be called when focus is cleared
     */
    public void addOptionManagerListener(OptionManagerListener optionManagerListener) {
        mOptionManagerListeners.add(optionManagerListener);
    }

    /**
     * Support legacy long click key action.
     * Perform a long click on the currently selected item, if that is possible. Long click is
     * possible only if an AccessibilityNodeActionNode is the only thing highlighted, and if
     * the corresponding AccessibilityNodeInfo accepts the long click action.
     * If the long click goes through, reset the focus.
     */
    public void performLongClick() {
        SwitchAccessNodeCompat compat = findCurrentlyActiveNode();
        if (compat != null) {
            if (compat.performAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK)) {
                clearFocus();
            }
            compat.recycle();
        }
    }

    /**
     * Support legacy scroll key actions.
     * Perform a scroll on the currently selected item, if it is scrollable, or a scrollable parent
     * if one can be found. If the scroll action is accepted, focus is cleared.
     * @param scrollAction Either {@code AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD} or
     * {@code AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD}.
     */
    public void performScrollAction(int scrollAction) {
        SwitchAccessNodeCompat compat = findCurrentlyActiveNode();
        while (compat != null) {
            if (compat.isScrollable()) {
                if (compat.performAction(scrollAction)) {
                    clearFocus();
                }
                compat.recycle();
                return;
            }
            SwitchAccessNodeCompat parent = compat.getParent();
            compat.recycle();
            compat = parent;
        }
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
        Context context = mOverlayController.getContext();
        mOptionScanningEnabled = SwitchAccessPreferenceActivity.isOptionScanningEnabled(context);
        String defaultWeight = context.getString(R.string.pref_highlight_weight_default);
        /* Configure highlighting */
        for (int i = 0; i < mOptionPaintArray.length; ++i) {
            /*
             * Always configure element 0 based on preferences. Only configure the others if we're
             * option scanning.
             */
            if ((i == 0) || mOptionScanningEnabled) {
                String hexStringColor = prefs.getString(mHighlightColorPrefKeys[i], mHighlightColorDefaults[i]);
                int color = Integer.parseInt(hexStringColor, 16);
                mOptionPaintArray[i].setColor(color);
                mOptionPaintArray[i].setAlpha(255);

                String stringWeight = prefs.getString(mHighlightWeightPrefKeys[i], defaultWeight);
                int weight = Integer.valueOf(stringWeight);
                DisplayMetrics dm = context.getResources().getDisplayMetrics();
                float strokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, weight, dm);
                mOptionPaintArray[i].setStrokeWidth(strokeWidth);
            } else {
                mOptionPaintArray[i].setColor(Color.TRANSPARENT);
            }
        }
        mStartScanAutomatically = prefs.getBoolean(context.getString(R.string.switch_access_auto_start_scan_key),
                false);
    }

    /**
     * Register a listener to notify of auto-scan activity
     * @param listener the listener to be set
     */
    public void setScanListener(ScanListener listener) {
        mScanListener = listener;
    }

    private void clearFocus() {
        mCurrentNode = null;
        mOverlayController.clearOverlay();
        for (OptionManagerListener listener : mOptionManagerListeners) {
            listener.onOptionManagerClearedFocus();
        }
    }

    private void onNodeFocused() {
        if (mScanListener != null) {
            if (mCurrentNode instanceof ClearFocusNode) {
                mScanListener.onScanCompletedWithNoSelection();
            } else if (mCurrentNode instanceof OptionScanActionNode) {
                mScanListener.onScanSelection();
            } else {
                mScanListener.onScanFocusChanged();
            }
        }
        mCurrentNode.performAction();

        /* TODO Any items that we want drawn on the screen could be directly grouped
         * into option groups when the tree is being constructed. That way the drawing of the
         * button or any other items would be data driven. */
        if (mCurrentNode instanceof OptionScanSelectionNode) {
            mOverlayController.clearOverlay();
            final OptionScanSelectionNode selectionNode = (OptionScanSelectionNode) mCurrentNode;
            if (mOptionScanningEnabled) {
                mOverlayController.drawMenuButton();
                /* showSelections() needs to know the location of the button in the screen to
                 * highlight it. Hence run it a handler to give the thread a chance to draw the
                 * overlay.
                 */
                new Handler().post(new Runnable() {
                    @Override
                    public void run() {
                        selectionNode.showSelections(mOverlayController, mOptionPaintArray);
                    }
                });
            } else {
                selectionNode.showSelections(mOverlayController, mOptionPaintArray);
            }
        } else {
            clearFocus();
        }
    }

    /*
     * Find exactly one {@code SwitchAccessNodeCompat} in the current tree
     * @return an {@code obtain}ed SwitchAccessNodeCompat if there is exactly one in the
     * current tree. Returns {@code null} otherwise.
     */
    private SwitchAccessNodeCompat findCurrentlyActiveNode() {
        if (!(mCurrentNode instanceof OptionScanSelectionNode)) {
            return null;
        }
        OptionScanNode startNode = ((OptionScanSelectionNode) mCurrentNode).getChild(OPTION_INDEX_CLICK);
        Set<AccessibilityNodeActionNode> nodeSet = new HashSet<>();
        addAccessibilityNodeActionNodesToSet(startNode, nodeSet);
        SwitchAccessNodeCompat compat = null;
        for (AccessibilityNodeActionNode actionNode : nodeSet) {
            SwitchAccessNodeCompat actionNodeCompat = actionNode.getNodeInfoCompat();
            if (actionNodeCompat == null) {
                continue; // Should never happen
            }
            if (compat == null) {
                compat = actionNodeCompat;
            } else if (compat.equals(actionNodeCompat)) {
                actionNodeCompat.recycle();
            } else {
                compat.recycle();
                actionNodeCompat.recycle();
                return null;
            }
        }
        return compat;
    }

    /*
     * Find all AccessibilityNodeActionNodes in the tree rooted at the current selection
     */
    private void addAccessibilityNodeActionNodesToSet(OptionScanNode startNode,
            Set<AccessibilityNodeActionNode> nodeSet) {
        if (startNode instanceof AccessibilityNodeActionNode) {
            nodeSet.add((AccessibilityNodeActionNode) startNode);
        }
        if (startNode instanceof OptionScanSelectionNode) {
            OptionScanSelectionNode selectionNode = (OptionScanSelectionNode) startNode;
            for (int i = 0; i < selectionNode.getChildCount(); ++i) {
                addAccessibilityNodeActionNodesToSet(selectionNode.getChild(i), nodeSet);
            }
        }
    }

    private OptionScanNode findLastSelectionNode() {
        OptionScanNode newNode = mRootNode;
        if (!(newNode instanceof OptionScanSelectionNode)) {
            return null;
        }
        OptionScanNode possibleNewNode = ((OptionScanSelectionNode) newNode).getChild(OPTION_INDEX_NEXT);
        while (possibleNewNode instanceof OptionScanSelectionNode) {
            newNode = possibleNewNode;
            possibleNewNode = ((OptionScanSelectionNode) newNode).getChild(OPTION_INDEX_NEXT);
        }
        return newNode;
    }

    /**
     * Interface to monitor when focus is cleared
     */
    public interface OptionManagerListener {
        /** Called when scanning is automatically started */
        void onOptionManagerStartedAutoScan();

        /** Called when focus clears */
        void onOptionManagerClearedFocus();
    }

    /**
     * Interface to monitor the user's progress of scanning to desired items
     */
    public interface ScanListener {
        /** Called when scanning starts and the first highlighting is drawn */
        void onScanStart();

        /** Called when scanning reaches a new selection node and highlighting changes */
        void onScanFocusChanged();

        /** Called when scanning reaches an action node and an action is taken */
        void onScanSelection();

        /** Called when scanning completes without any action being taken */
        void onScanCompletedWithNoSelection();
    }
}