Java tutorial
/* * Copyright (C) 2014 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.utils; import android.accessibilityservice.AccessibilityService; import android.annotation.TargetApi; import android.content.Context; import android.content.DialogInterface; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.PixelFormat; import android.graphics.PorterDuff.Mode; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable.Orientation; import android.os.Build; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.text.TextUtils; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.accessibility.AccessibilityNodeInfo; import com.android.utils.labeling.CustomLabelManager; import com.android.utils.traversal.NodeFocusFinder; import com.android.utils.widget.SimpleOverlay; import java.util.ArrayList; import java.util.List; /** * Facilitates search of the nodes on the screen. Nodes are matched by description, and the * accessibility focus is moved to the matched node. */ public class NodeSearch { /** * A formatter that determines the text to display given the search query, and the size at * which that text should be displayed. */ public interface SearchTextFormatter { /** * Get the size, in pixels, at which the text returned by {@link #getDisplayText} should be * displayed. * * @return The desired text size. */ public float getTextSize(); /** * Get the text that should be displayed for a certain search query. * * @param queryText The search query. * @return The text that should be displayed for this search. */ public String getDisplayText(String queryText); } /** * A filter that may exclude some nodes from the search. */ public interface SearchResultFilter { /** * Check if a node should be excluded from the search. * * @param node The node that is being checked. * @return {@code true} if the node should be excluded, or {@code false} otherwise. */ public boolean shouldFilter(AccessibilityNodeInfoCompat node); } /** The current search query. */ private final StringBuilder mQueryText = new StringBuilder(); /** The last matched node for the current search. */ private final AccessibilityNodeInfoRef mMatchedNode = new AccessibilityNodeInfoRef(); /** The accessibility service. */ private final AccessibilityService mAccessibilityService; /** The label manager used to obtain node descriptions. */ private final CustomLabelManager mLabelManager; /** The search result filters that should be used, if any. */ private final List<SearchResultFilter> mFilters = new ArrayList<>(); /** The overlay that is used to show the current search. */ private final SearchOverlay mSearchOverlay; /** Whether or not there is an active search. */ private boolean mActive; /** * Create a new NodeSearch instance. * * @param accessibilityService The accessibility service. * @param labelManager The custom label manager, or {@code null} if the API version does not * support custom labels. * @param textFormatter The formatter for the search display. */ public NodeSearch(AccessibilityService accessibilityService, CustomLabelManager labelManager, SearchTextFormatter textFormatter) { mAccessibilityService = accessibilityService; mLabelManager = labelManager; mSearchOverlay = new SearchOverlay(accessibilityService, mQueryText, textFormatter); } /** * Create a new NodeSearch instance with filters. * * @param accessibilityService The accessibility service. * @param labelManager The custom label manager, or {@code null} if the API version does not * support custom labels. * @param textFormatter The formatter for the search display. * @param filters The filters that should be used. */ public NodeSearch(AccessibilityService accessibilityService, CustomLabelManager labelManager, SearchTextFormatter textFormatter, List<SearchResultFilter> filters) { this(accessibilityService, labelManager, textFormatter); mFilters.addAll(filters); } /** * Start a search. Shows the search overlay. */ public void startSearch() { mSearchOverlay.show(); mActive = true; } /** * Stop the current search. Hides the search overlay and clears the search query. */ public void stopSearch() { mMatchedNode.clear(); mQueryText.setLength(0); mSearchOverlay.hide(); mActive = false; } /** * Check if this instance is actively handling a search. * * @return {@code true} if there is an active search, or {@code false} otherwise. */ public boolean isActive() { return mActive; } /** * Try to add some text to the search query. The text is only added if there are search results * for the new query, in which case the matched node may change. * * @param newText The text to add. * @return {@code true} if the text was added successfully, or {@code false} otherwise. */ public boolean tryAddQueryText(CharSequence newText) { int initLength = mQueryText.length(); mQueryText.append(newText); if (evaluateSearch()) { mSearchOverlay.refreshOverlay(); return true; } // Search failed, go back to old text. mQueryText.delete(initLength, mQueryText.length()); return false; } /** * Delete the last entered character if it exists. Has no effect on the matched node. * * @return {@code true} if a character was successfully deleted, or {@code false} if there was * no character to delete. */ public boolean backspaceQueryText() { int length = mQueryText.length(); if (length > 0) { mQueryText.deleteCharAt(length - 1); mSearchOverlay.refreshOverlay(); return true; } return false; } /** * Get the current search query. * * @return The current search query. May be empty. */ public String getCurrentQuery() { return mQueryText.toString(); } /** * Get the text of the currently matched node. * * @return The text of the current match. May be empty, for example if no match has been found. */ public String getMatchText() { final AccessibilityNodeInfoCompat currentMatch = mMatchedNode.get(); if (currentMatch == null) { return ""; } final CharSequence nodeText = AccessibilityNodeInfoUtils.getNodeText(currentMatch, mLabelManager); if (nodeText == null) { return ""; } return nodeText.toString(); } /** * Searches for the next result matching the current search query in the specified direction. * Ordering of results taken from linear navigation. * * @param direction The direction in which to search, {@link NodeFocusFinder#SEARCH_FORWARD} or * {@link NodeFocusFinder#SEARCH_BACKWARD}. * @return {@code true} if a match was found, or {@code false} otherwise. */ public boolean nextResult(int direction) { AccessibilityNodeInfoRef next = new AccessibilityNodeInfoRef(); next.reset(NodeFocusFinder.focusSearch(getCurrentNode(), direction)); AccessibilityNodeInfoCompat focusableNext = null; try { while (next.get() != null) { if (nodeMatchesQuery(next.get())) { // Even if the text matches, we need to make sure the node should be focused or // has a parent that should be focused. focusableNext = AccessibilityNodeInfoUtils.findFocusFromHover(next.get()); // Only count this as a match if it doesn't lead to the same parent. if (focusableNext != null && !focusableNext.isAccessibilityFocused()) { break; } } next.reset(NodeFocusFinder.focusSearch(next.get(), direction)); if (focusableNext != null) { focusableNext.recycle(); focusableNext = null; } } if (focusableNext == null) { return false; } mMatchedNode.reset(next); return PerformActionUtils.performAction(focusableNext, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); } finally { if (focusableNext != null) { focusableNext.recycle(); } next.recycle(); } } /** * Re-evaluate the search (perhaps, for example, because the screen content changed). */ public void reEvaluateSearch() { mMatchedNode.reset(AccessibilityNodeInfoUtils.refreshNode(mMatchedNode.get())); evaluateSearch(); } /** * Check if the search has found anything. * * @return {@code true} if a match has been found, or {@code false} otherwise. */ public boolean hasMatch() { return !AccessibilityNodeInfoRef.isNull(mMatchedNode); } /** * Get the last matching node, if available, or the currently focused node otherwise. * * @return The last matched node, if a match was previously made. If no match has been made yet, * returns the currently focused node. */ AccessibilityNodeInfoCompat getCurrentNode() { return AccessibilityNodeInfoRef.isNull(mMatchedNode) ? FocusFinder.getFocusedNode(mAccessibilityService, true) : mMatchedNode.get(); } /** * Evaluates the search with the current query, searching from the last matched node forward. * * @return {@code true} if a match was found, or {@code false} otherwise. */ private boolean evaluateSearch() { // First check if current selected result still matches. return nodeMatchesQuery(mMatchedNode.get()) || nextResult(NodeFocusFinder.SEARCH_FORWARD); } /** * Check if the specified node's description matches the current query text (case insensitive). * * @param node The node to check. * @return {@code true} if the node's description contains the current query text (ignoring * case), or {@code false} otherwise. */ private boolean nodeMatchesQuery(AccessibilityNodeInfoCompat node) { // When no query text, consider everything a match. if (TextUtils.isEmpty(mQueryText)) { return AccessibilityNodeInfoUtils.shouldFocusNode(node); } if (node == null) { return false; } for (SearchResultFilter filter : mFilters) { if (filter.shouldFilter(node)) { return false; } } CharSequence nodeText = AccessibilityNodeInfoUtils.getNodeText(node, mLabelManager); if (nodeText == null) { return false; } String queryText = mQueryText.toString().toLowerCase(); return nodeText.toString().toLowerCase().contains(queryText); } /* Package private methods for testing. */ /* package */ void setQueryTextForTest(String text) { mQueryText.setLength(0); mQueryText.append(text); } /** * Controls the view that shows search overlay content. */ private static class SearchOverlay extends SimpleOverlay implements DialogInterface { /** The search view. */ private final SearchView mSearchView; /** * Creates the overlay with it initially invisible. */ public SearchOverlay(Context context, StringBuilder queryText, SearchTextFormatter textFormatter) { super(context); mSearchView = new SearchView(context, queryText, textFormatter); // Make overlay appear on everything it can. LayoutParams params = getParams(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { params.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; } else { params.type = LayoutParams.TYPE_SYSTEM_ERROR; } setParams(params); setContentView(mSearchView); } /** * Called when the the overlay is shown. */ @Override protected void onShow() { mSearchView.show(); } /** * Refreshes the overlaid text display. */ public void refreshOverlay() { mSearchView.invalidate(); } @Override public void cancel() { dismiss(); } @Override public void dismiss() { // This also effectively hides the search view. hide(); } } /** * View handling drawing of incremental search overlay. */ private static class SearchView extends SurfaceView { /** The colors to use for the gradient background. */ private static final int GRADIENT_INNER_COLOR = 1996488704; // #7000 private static final int GRADIENT_OUTER_COLOR = 1996488704; /** The surface holder onto which the view is drawn. */ private SurfaceHolder mHolder; /** The background. */ private final GradientDrawable mGradientBackground; /** The formatter for the text that will be displayed in this view. */ private final SearchTextFormatter mTextFormatter; /** * The search query text. Synced to the StringBuilder in NodeSearch so we shouldn't * modify it here. */ private final StringBuilder mQueryText; private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { mHolder = holder; } @Override public void surfaceDestroyed(SurfaceHolder holder) { mHolder = null; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { invalidate(); } }; public SearchView(Context context, StringBuilder queryText, SearchTextFormatter textFormatter) { super(context); mQueryText = queryText; mTextFormatter = textFormatter; final SurfaceHolder holder = getHolder(); holder.setFormat(PixelFormat.TRANSLUCENT); holder.addCallback(mSurfaceCallback); // Gradient colors. final int[] colors = new int[] { GRADIENT_INNER_COLOR, GRADIENT_OUTER_COLOR }; mGradientBackground = new GradientDrawable(Orientation.TOP_BOTTOM, colors); mGradientBackground.setGradientType(GradientDrawable.LINEAR_GRADIENT); } public void show() { invalidate(); } @Override public void invalidate() { super.invalidate(); final SurfaceHolder holder = mHolder; if (holder == null) { return; } final Canvas canvas = holder.lockCanvas(); if (canvas == null) { return; } // Clear the canvas. canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); if (getVisibility() != View.VISIBLE) { holder.unlockCanvasAndPost(canvas); return; } final int width = getWidth(); final int height = getHeight(); // Draw the pretty gradient background. mGradientBackground.setBounds(0, 0, width, height); mGradientBackground.draw(canvas); Paint paint = new Paint(); paint.setColor(Color.WHITE); paint.setStyle(Style.FILL); paint.setTextAlign(Align.CENTER); paint.setTextSize(mTextFormatter.getTextSize()); canvas.drawText(mTextFormatter.getDisplayText(mQueryText.toString()), width / 2.0f, height / 2.0f, paint); holder.unlockCanvasAndPost(canvas); } } }