| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * 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.systemui.accessibility.accessibilitymenu.view; |
| |
| import static android.view.Display.DEFAULT_DISPLAY; |
| |
| import static java.lang.Math.max; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.content.res.Configuration; |
| import android.graphics.Insets; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.hardware.display.DisplayManager; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.view.Display; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.Surface; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowInsets; |
| import android.view.WindowManager; |
| import android.view.WindowMetrics; |
| import android.view.accessibility.AccessibilityManager; |
| import android.widget.FrameLayout; |
| import android.widget.TextView; |
| |
| import androidx.annotation.NonNull; |
| |
| import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; |
| import com.android.systemui.accessibility.accessibilitymenu.R; |
| import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment; |
| import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Provides functionality for Accessibility menu layout in a11y menu overlay. There are functions to |
| * configure or update Accessibility menu layout when orientation and display size changed, and |
| * functions to toggle menu visibility when button clicked or screen off. |
| */ |
| public class A11yMenuOverlayLayout { |
| |
| /** Predefined default shortcuts when large button setting is off. */ |
| private static final int[] SHORTCUT_LIST_DEFAULT = { |
| A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal() |
| }; |
| |
| /** Predefined default shortcuts when large button setting is on. */ |
| private static final int[] LARGE_SHORTCUT_LIST_DEFAULT = { |
| A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(), |
| A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal() |
| }; |
| |
| |
| |
| private final AccessibilityMenuService mService; |
| private final WindowManager mWindowManager; |
| private final DisplayManager mDisplayManager; |
| private ViewGroup mLayout; |
| private WindowManager.LayoutParams mLayoutParameter; |
| private A11yMenuViewPager mA11yMenuViewPager; |
| private Handler mHandler; |
| private AccessibilityManager mAccessibilityManager; |
| |
| public A11yMenuOverlayLayout(AccessibilityMenuService service) { |
| mService = service; |
| mWindowManager = mService.getSystemService(WindowManager.class); |
| mDisplayManager = mService.getSystemService(DisplayManager.class); |
| configureLayout(); |
| mHandler = new Handler(Looper.getMainLooper()); |
| mAccessibilityManager = mService.getSystemService(AccessibilityManager.class); |
| } |
| |
| /** Creates Accessibility menu layout and configure layout parameters. */ |
| public View configureLayout() { |
| return configureLayout(A11yMenuViewPager.DEFAULT_PAGE_INDEX); |
| } |
| |
| // TODO(b/78292783): Find a better way to inflate layout in the test. |
| /** |
| * Creates Accessibility menu layout, configure layout parameters and apply index to ViewPager. |
| * |
| * @param pageIndex the index of the ViewPager to show. |
| */ |
| public View configureLayout(int pageIndex) { |
| |
| int lastVisibilityState = View.GONE; |
| if (mLayout != null) { |
| lastVisibilityState = mLayout.getVisibility(); |
| mWindowManager.removeView(mLayout); |
| mLayout = null; |
| } |
| |
| if (mLayoutParameter == null) { |
| initLayoutParams(); |
| } |
| |
| final Display display = mService.getSystemService( |
| DisplayManager.class).getDisplay(DEFAULT_DISPLAY); |
| |
| mLayout = new FrameLayout( |
| mService.createDisplayContext(display).createWindowContext( |
| WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, null)); |
| updateLayoutPosition(); |
| inflateLayoutAndSetOnTouchListener(mLayout); |
| mA11yMenuViewPager = new A11yMenuViewPager(mService); |
| mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex); |
| mWindowManager.addView(mLayout, mLayoutParameter); |
| mLayout.setVisibility(lastVisibilityState); |
| |
| return mLayout; |
| } |
| |
| /** Updates view layout with new layout parameters only. */ |
| public void updateViewLayout() { |
| if (mLayout == null || mLayoutParameter == null) { |
| return; |
| } |
| updateLayoutPosition(); |
| mWindowManager.updateViewLayout(mLayout, mLayoutParameter); |
| } |
| |
| private void initLayoutParams() { |
| mLayoutParameter = new WindowManager.LayoutParams(); |
| mLayoutParameter.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; |
| mLayoutParameter.format = PixelFormat.TRANSLUCENT; |
| mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; |
| mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; |
| mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name)); |
| } |
| |
| private void inflateLayoutAndSetOnTouchListener(ViewGroup view) { |
| LayoutInflater inflater = LayoutInflater.from(mService); |
| inflater.inflate(R.layout.paged_menu, view); |
| view.setOnTouchListener(mService); |
| } |
| |
| /** |
| * Loads shortcut data from default shortcut ID array. |
| * |
| * @return A list of default shortcuts |
| */ |
| private List<A11yMenuShortcut> createShortcutList() { |
| List<A11yMenuShortcut> shortcutList = new ArrayList<>(); |
| |
| for (int shortcutId : |
| (A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService) |
| ? LARGE_SHORTCUT_LIST_DEFAULT : SHORTCUT_LIST_DEFAULT)) { |
| shortcutList.add(new A11yMenuShortcut(shortcutId)); |
| } |
| return shortcutList; |
| } |
| |
| /** Updates a11y menu layout position by configuring layout params. */ |
| private void updateLayoutPosition() { |
| final Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); |
| final Configuration configuration = mService.getResources().getConfiguration(); |
| final int orientation = configuration.orientation; |
| if (display != null && orientation == Configuration.ORIENTATION_LANDSCAPE) { |
| final boolean ltr = configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; |
| switch (display.getRotation()) { |
| case Surface.ROTATION_0: |
| case Surface.ROTATION_180: |
| mLayoutParameter.gravity = |
| (ltr ? Gravity.END : Gravity.START) | Gravity.BOTTOM |
| | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; |
| mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT; |
| mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT; |
| mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; |
| mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; |
| mLayout.setBackgroundResource(R.drawable.shadow_90deg); |
| break; |
| case Surface.ROTATION_90: |
| case Surface.ROTATION_270: |
| mLayoutParameter.gravity = |
| (ltr ? Gravity.START : Gravity.END) | Gravity.BOTTOM |
| | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; |
| mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT; |
| mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT; |
| mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; |
| mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; |
| mLayout.setBackgroundResource(R.drawable.shadow_270deg); |
| break; |
| default: |
| break; |
| } |
| } else { |
| mLayoutParameter.gravity = Gravity.BOTTOM; |
| mLayoutParameter.width = WindowManager.LayoutParams.MATCH_PARENT; |
| mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT; |
| mLayout.setBackgroundResource(R.drawable.shadow_0deg); |
| } |
| |
| // Adjusts the y position of a11y menu layout to make the layout not to overlap bottom |
| // navigation bar window. |
| updateLayoutByWindowInsetsIfNeeded(); |
| mLayout.setOnApplyWindowInsetsListener( |
| (view, insets) -> { |
| if (updateLayoutByWindowInsetsIfNeeded()) { |
| mWindowManager.updateViewLayout(mLayout, mLayoutParameter); |
| } |
| return view.onApplyWindowInsets(insets); |
| }); |
| } |
| |
| /** |
| * Returns {@code true} if the a11y menu layout params |
| * should be updated by {@link WindowManager} immediately due to window insets change. |
| * This method adjusts the layout position and size to |
| * make a11y menu not to overlap navigation bar window. |
| */ |
| private boolean updateLayoutByWindowInsetsIfNeeded() { |
| boolean shouldUpdateLayout = false; |
| WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); |
| Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility( |
| WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); |
| int xOffset = max(windowInsets.left, windowInsets.right); |
| int yOffset = windowInsets.bottom; |
| Rect windowBound = windowMetrics.getBounds(); |
| if (mLayoutParameter.x != xOffset || mLayoutParameter.y != yOffset) { |
| mLayoutParameter.x = xOffset; |
| mLayoutParameter.y = yOffset; |
| shouldUpdateLayout = true; |
| } |
| // for gestural navigation mode and the landscape mode, |
| // the layout height should be decreased by system bar |
| // and display cutout inset to fit the new |
| // frame size that doesn't overlap the navigation bar window. |
| int orientation = mService.getResources().getConfiguration().orientation; |
| if (mLayout.getHeight() != mLayoutParameter.height |
| && orientation == Configuration.ORIENTATION_LANDSCAPE) { |
| mLayoutParameter.height = windowBound.height() - yOffset; |
| shouldUpdateLayout = true; |
| } |
| return shouldUpdateLayout; |
| } |
| |
| /** |
| * Gets the current page index when device configuration changed. {@link |
| * AccessibilityMenuService#onConfigurationChanged(Configuration)} |
| * |
| * @return the current index of the ViewPager. |
| */ |
| public int getPageIndex() { |
| if (mA11yMenuViewPager != null) { |
| return mA11yMenuViewPager.mViewPager.getCurrentItem(); |
| } |
| return A11yMenuViewPager.DEFAULT_PAGE_INDEX; |
| } |
| |
| /** |
| * Hides a11y menu layout. And return if layout visibility has been changed. |
| * |
| * @return {@code true} layout visibility is toggled off; {@code false} is unchanged |
| */ |
| public boolean hideMenu() { |
| if (mLayout.getVisibility() == View.VISIBLE) { |
| mLayout.setVisibility(View.GONE); |
| return true; |
| } |
| return false; |
| } |
| |
| /** Toggles a11y menu layout visibility. */ |
| public void toggleVisibility() { |
| mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE); |
| } |
| |
| /** Shows hint text on a minimal Snackbar-like text view. */ |
| public void showSnackbar(String text) { |
| final int animationDurationMs = 300; |
| final int timeoutDurationMs = mAccessibilityManager.getRecommendedTimeoutMillis(2000, |
| AccessibilityManager.FLAG_CONTENT_TEXT); |
| |
| final TextView snackbar = mLayout.findViewById(R.id.snackbar); |
| snackbar.setText(text); |
| |
| // Remove any existing fade-out animation before starting any new animations. |
| mHandler.removeCallbacksAndMessages(null); |
| |
| if (snackbar.getVisibility() != View.VISIBLE) { |
| snackbar.setAlpha(0f); |
| snackbar.setVisibility(View.VISIBLE); |
| snackbar.animate().alpha(1f).setDuration(animationDurationMs).setListener(null); |
| } |
| mHandler.postDelayed(() -> snackbar.animate().alpha(0f).setDuration( |
| animationDurationMs).setListener( |
| new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(@NonNull Animator animation) { |
| snackbar.setVisibility(View.GONE); |
| } |
| }), timeoutDurationMs); |
| } |
| } |