/* * 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.annotation.TargetApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; import android.os.Build; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.view.WindowManager; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import com.android.talkback.R; import com.android.utils.widget.SimpleOverlay; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Controller for the Switch Access overlay. The controller handles two operations: it outlines * groups of Views, and it presents context menus (with Views that are outlined). */ @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) public class OverlayController { private final SimpleOverlay mOverlay; private final RelativeLayout mRelativeLayout; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { configureOverlay(); } } }; /* * TODO replace ugly map with a better solution. The better solution will likely change * the preferences, which this approach avoids touching. */ private static final Map<Integer, Integer> MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP; static { MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP = new HashMap<>(); MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP.put(new Integer(0xff4caf50), new Integer(0xff1b5e20)); MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP.put(new Integer(0xffff9800), new Integer(0xffe65100)); MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP.put(new Integer(0xfff44336), new Integer(0xffb71c1c)); MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP.put(new Integer(0xff2196f3), new Integer(0xff0d47a1)); MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP.put(new Integer(0xffffffff), new Integer(0xff000000)); } /** * @param overlay The overlay on which to draw focus indications */ public OverlayController(SimpleOverlay overlay) { mOverlay = overlay; mOverlay.setContentView(R.layout.switch_access_overlay_layout); mRelativeLayout = (RelativeLayout) mOverlay.findViewById(R.id.overlayRelativeLayout); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); mOverlay.getContext().registerReceiver(mBroadcastReceiver, filter); } /** * Get the overlay ready to show. This method displays an empty overlay and tweaks it to * make sure it's in the right location. */ public void configureOverlay() { configureOverlayBeforeShow(); mOverlay.show(); new Handler().post(new Runnable() { @Override public void run() { configureOverlayAfterShow(); } }); } public void highlightPerimeterOfRects(Iterable<Rect> rects, Paint highlightPaint) { final Set<Rect> rectsToHighlight = new HashSet<>(); final Paint finalHighlightPaint = new Paint(highlightPaint); for (Rect rect : rects) { rectsToHighlight.add(rect); } mOverlay.show(); /* * Run the rest of the function in a handler to give the thread a chance to draw the * overlay. */ new Handler().post(new Runnable() { @Override public void run() { int[] layoutCoordinates = new int[2]; mRelativeLayout.getLocationOnScreen(layoutCoordinates); for (Rect rect : rectsToHighlight) { ShapeDrawable mainHighlightDrawable = new ShapeDrawable(new RectShape()); mainHighlightDrawable.setIntrinsicWidth(rect.width()); mainHighlightDrawable.setIntrinsicHeight(rect.height()); mainHighlightDrawable.getPaint().set(finalHighlightPaint); Drawable highlightDrawable = mainHighlightDrawable; if (MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP .containsKey(finalHighlightPaint.getColor())) { ShapeDrawable outerHighlightDrawable = new ShapeDrawable(new RectShape()); outerHighlightDrawable.setIntrinsicWidth(rect.width()); outerHighlightDrawable.setIntrinsicHeight(rect.height()); Paint outerHighlightPaint = new Paint(finalHighlightPaint); outerHighlightPaint.setColor(MAIN_TO_OUTER_HIGHLIGHT_COLOR_MAP .get(finalHighlightPaint.getColor())); outerHighlightPaint .setStrokeWidth(finalHighlightPaint.getStrokeWidth() / 2); outerHighlightDrawable.getPaint().set(outerHighlightPaint); Drawable[] layers = {mainHighlightDrawable, outerHighlightDrawable}; highlightDrawable = new LayerDrawable(layers); } ImageView imageView = new ImageView(mOverlay.getContext()); imageView.setBackground(highlightDrawable); // Align image with node we're highlighting final RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(rect.width(), rect.height()); layoutParams.leftMargin = rect.left - layoutCoordinates[0]; layoutParams.topMargin = rect.top - layoutCoordinates[1]; imageView.setLayoutParams(layoutParams); mRelativeLayout.addView(imageView); } } }); } /** * Override focus highlighting with a custom overlay */ public void addViewAndShow(View view) { mRelativeLayout.addView(view); mOverlay.show(); } /** * Clear focus highlighting */ public void clearOverlay() { mRelativeLayout.removeAllViews(); mOverlay.hide(); } /** * Shut down nicely */ public void shutdown() { mOverlay.getContext().unregisterReceiver(mBroadcastReceiver); mOverlay.hide(); } /** * When option scanning is enabled, a menu button is drawn at the top of the screen. This * button offers the user the possibility of clearing the focus or choosing global actions * (i.e Home, Back, Notifications, etc). */ public void drawMenuButton() { Context context = mOverlay.getContext(); LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); LinearLayout menuButtonLayout = (LinearLayout) layoutInflater .inflate(R.layout.switch_access_global_menu_button, mRelativeLayout, false); addViewAndShow(menuButtonLayout); } /** * @return If option scanning is enabled, gets the location of the menu at the top of the * screen. Otherwise null is returned. */ public Rect getMenuButtonLocation() { Button menuButton = (Button) mOverlay.findViewById(R.id.top_screen_menu_button); if (menuButton != null) { Rect locationOnScreen = new Rect(); menuButton.getGlobalVisibleRect(locationOnScreen); return locationOnScreen; } return null; } /** * Obtain the context for drawing */ public Context getContext() { return mOverlay.getContext(); } private void configureOverlayBeforeShow() { // The overlay shouldn't capture touch events final WindowManager.LayoutParams params = mOverlay.getParams(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { params.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; } else { params.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; } params.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; /* The overlay covers the entire screen. However, there is a left, top, right, and * bottom margin. */ final WindowManager wm = (WindowManager) mOverlay.getContext() .getSystemService(Context.WINDOW_SERVICE); final Point size = new Point(); wm.getDefaultDisplay().getRealSize(size); params.height = size.y; params.width = size.x; params.x = 0; params.y = 0; mOverlay.setParams(params); } /* * For some reason, it's very difficult to create a layout that covers exactly the entire screen * and doesn't move when an unhandled key is pressed. The configuration we're using seems to * result in a layout that starts above the screen. So we split initialization into two * pieces, and here we find out where the overlay ended up and move it to be at the top * of the screen. * TODO Separating the menu and highlighting should be a cleaner way to solve this * issue */ private void configureOverlayAfterShow() { int[] location = new int[2]; mRelativeLayout.getLocationOnScreen(location); WindowManager.LayoutParams layoutParams = mOverlay.getParams(); layoutParams.y -= location[1]; mOverlay.setParams(layoutParams); } }