/* * 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.treebuilding; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Point; import android.graphics.Rect; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import com.android.switchaccess.ContextMenuItem; import com.android.switchaccess.OptionScanActionNode; import com.android.switchaccess.OptionScanNode; import com.android.switchaccess.SwitchAccessNodeCompat; import com.android.switchaccess.SwitchAccessWindowInfo; import com.android.talkback.R; import com.android.utils.SharedPreferencesUtils; import android.view.Display; import android.view.WindowManager; import android.view.accessibility.AccessibilityWindowInfo; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; /** * Builder that constructs a hierarchy to scan from a list of windows */ public class MainTreeBuilder extends TreeBuilder { private final RowColumnTreeBuilder mRowColumnTreeBuilder; private final LinearScanTreeBuilder mLinearScanTreeBuilder; private final TalkBackOrderNDegreeTreeBuilder mOptionScanTreeBuilder; private TreeBuilder mBuilderForViews; private boolean mOptionScanningEnabled; /** * @param context A valid context for interacting with the framework */ public MainTreeBuilder(Context context) { super(context); mLinearScanTreeBuilder = new LinearScanTreeBuilder(context); mRowColumnTreeBuilder = new RowColumnTreeBuilder(context); mOptionScanTreeBuilder = new TalkBackOrderNDegreeTreeBuilder(context); updatePrefs(SharedPreferencesUtils.getSharedPreferences(mContext)); } @VisibleForTesting public MainTreeBuilder(Context context, LinearScanTreeBuilder linearScanTreeBuilder, RowColumnTreeBuilder rowColumnTreeBuilder, TalkBackOrderNDegreeTreeBuilder talkBackOrderNDegreeTreeBuilder) { super(context); mLinearScanTreeBuilder = linearScanTreeBuilder; mRowColumnTreeBuilder = rowColumnTreeBuilder; mOptionScanTreeBuilder = talkBackOrderNDegreeTreeBuilder; } @Override public OptionScanNode addWindowListToTree(List<SwitchAccessWindowInfo> windowList, OptionScanNode tree) { if (windowList != null) { List<SwitchAccessWindowInfo> wList = new ArrayList<>(windowList); sortWindowListForTraversalOrder(wList); removeSystemButtonsWindowFromWindowList(wList); if (mOptionScanningEnabled) { return mOptionScanTreeBuilder.addWindowListToTree(wList, tree); } for (SwitchAccessWindowInfo window : wList) { SwitchAccessNodeCompat windowRoot = window.getRoot(); if (windowRoot != null) { if (window.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD) { tree = mRowColumnTreeBuilder.addViewHierarchyToTree(windowRoot, tree); } else { tree = addViewHierarchyToTree(windowRoot, tree); } windowRoot.recycle(); } } } return tree; } @Override public OptionScanNode addViewHierarchyToTree(SwitchAccessNodeCompat root, OptionScanNode treeToBuildOn) { return mBuilderForViews.addViewHierarchyToTree(root, treeToBuildOn); } @Override public OptionScanNode buildContextMenu(List<? extends ContextMenuItem> actionList) { TreeBuilder builder = mOptionScanningEnabled ? mOptionScanTreeBuilder : mLinearScanTreeBuilder; return builder.buildContextMenu(actionList); } /** * Sort windows so that the IME is traversed first, and the system windows last. Note that * the list comes out backwards, which makes it easy to iterate through it when building the * tree from the bottom up. * @param windowList The list to be sorted. */ private static void sortWindowListForTraversalOrder(List<SwitchAccessWindowInfo> windowList) { Collections.sort(windowList, new Comparator<SwitchAccessWindowInfo>() { @Override public int compare(SwitchAccessWindowInfo arg0, SwitchAccessWindowInfo arg1) { // Compare based on window type final int type0 = arg0.getType(); final int type1 = arg1.getType(); if (type0 == type1) { return 0; } /* Present IME windows first */ if (type0 == AccessibilityWindowInfo.TYPE_INPUT_METHOD) { return 1; } if (type1 == AccessibilityWindowInfo.TYPE_INPUT_METHOD) { return -1; } /* Present system windows last */ if (type0 == AccessibilityWindowInfo.TYPE_SYSTEM) { return -1; } if (type1 == AccessibilityWindowInfo.TYPE_SYSTEM) { return 1; } /* Others are don't care */ return 0; } }); } /* * Remove the window with system buttons (BACK, HOME, RECENTS) from the window list. We * remove them because on some (but not all - see BUG) devices, the highlight rectangles * don't show up on around the system buttons. */ private void removeSystemButtonsWindowFromWindowList(List<SwitchAccessWindowInfo> windowList) { final WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); final Display display = wm.getDefaultDisplay(); final Point screenSize = new Point(); display.getSize(screenSize); final Iterator<SwitchAccessWindowInfo> windowIterator = windowList.iterator(); while (windowIterator.hasNext()) { SwitchAccessWindowInfo window = windowIterator.next(); /* Keep all non-system buttons */ if (window.getType() != AccessibilityWindowInfo.TYPE_SYSTEM) { continue; } final Rect windowBounds = new Rect(); window.getBoundsInScreen(windowBounds); /* Keep system dialogs (app has crashed), which don't border any edge */ if ((windowBounds.top > 0) && (windowBounds.bottom < screenSize.y) && (windowBounds.left > 0) && (windowBounds.right < screenSize.x)) { continue; } /* Keep notifications, which start at the top and cover more than half the width */ if ((windowBounds.top <= 0) && (windowBounds.width() > screenSize.x / 2)) { continue; } /* Keep large system overlays like the context menu */ final int windowArea = windowBounds.width() * windowBounds.height(); final int screenArea = screenSize.x * screenSize.y; if (windowArea > (screenArea / 2)) { continue; } windowIterator.remove(); } } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { super.onSharedPreferenceChanged(prefs, key); updatePrefs(prefs); } private void updatePrefs(SharedPreferences prefs) { String viewLinearImeRowColKey = mContext.getString(R.string.views_linear_ime_row_col_key); String optionScanKey = mContext.getString(R.string.option_scanning_key); String scanPref = prefs.getString( mContext.getString(R.string.pref_scanning_methods_key), mContext.getString(R.string.pref_scanning_methods_default)); mOptionScanningEnabled = TextUtils.equals(scanPref, optionScanKey); mBuilderForViews = (TextUtils.equals(scanPref, viewLinearImeRowColKey)) ? mLinearScanTreeBuilder : mRowColumnTreeBuilder; } @Override public void shutdown() { super.shutdown(); mLinearScanTreeBuilder.shutdown(); mRowColumnTreeBuilder.shutdown(); mOptionScanTreeBuilder.shutdown(); } }