1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility.accessibilitymenu.view; 18 19 import static android.view.Display.DEFAULT_DISPLAY; 20 21 import static java.lang.Math.max; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.content.res.Configuration; 26 import android.graphics.Insets; 27 import android.graphics.PixelFormat; 28 import android.graphics.Rect; 29 import android.hardware.display.DisplayManager; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.view.Display; 33 import android.view.Gravity; 34 import android.view.LayoutInflater; 35 import android.view.Surface; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.WindowInsets; 39 import android.view.WindowManager; 40 import android.view.WindowMetrics; 41 import android.view.accessibility.AccessibilityManager; 42 import android.widget.FrameLayout; 43 import android.widget.TextView; 44 45 import androidx.annotation.NonNull; 46 47 import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; 48 import com.android.systemui.accessibility.accessibilitymenu.R; 49 import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment; 50 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 55 /** 56 * Provides functionality for Accessibility menu layout in a11y menu overlay. There are functions to 57 * configure or update Accessibility menu layout when orientation and display size changed, and 58 * functions to toggle menu visibility when button clicked or screen off. 59 */ 60 public class A11yMenuOverlayLayout { 61 62 /** Predefined default shortcuts when large button setting is off. */ 63 private static final int[] SHORTCUT_LIST_DEFAULT = { 64 A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(), 65 A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(), 66 A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(), 67 A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(), 68 A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(), 69 A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(), 70 A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(), 71 A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(), 72 A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(), 73 A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(), 74 A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(), 75 A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal() 76 }; 77 78 /** Predefined default shortcuts when large button setting is on. */ 79 private static final int[] LARGE_SHORTCUT_LIST_DEFAULT = { 80 A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(), 81 A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(), 82 A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(), 83 A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(), 84 A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(), 85 A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(), 86 A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(), 87 A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(), 88 A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(), 89 A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(), 90 A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(), 91 A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal() 92 }; 93 94 95 96 private final AccessibilityMenuService mService; 97 private final WindowManager mWindowManager; 98 private final DisplayManager mDisplayManager; 99 private ViewGroup mLayout; 100 private WindowManager.LayoutParams mLayoutParameter; 101 private A11yMenuViewPager mA11yMenuViewPager; 102 private Handler mHandler; 103 private AccessibilityManager mAccessibilityManager; 104 A11yMenuOverlayLayout(AccessibilityMenuService service)105 public A11yMenuOverlayLayout(AccessibilityMenuService service) { 106 mService = service; 107 mWindowManager = mService.getSystemService(WindowManager.class); 108 mDisplayManager = mService.getSystemService(DisplayManager.class); 109 configureLayout(); 110 mHandler = new Handler(Looper.getMainLooper()); 111 mAccessibilityManager = mService.getSystemService(AccessibilityManager.class); 112 } 113 114 /** Creates Accessibility menu layout and configure layout parameters. */ configureLayout()115 public View configureLayout() { 116 return configureLayout(A11yMenuViewPager.DEFAULT_PAGE_INDEX); 117 } 118 119 // TODO(b/78292783): Find a better way to inflate layout in the test. 120 /** 121 * Creates Accessibility menu layout, configure layout parameters and apply index to ViewPager. 122 * 123 * @param pageIndex the index of the ViewPager to show. 124 */ configureLayout(int pageIndex)125 public View configureLayout(int pageIndex) { 126 127 int lastVisibilityState = View.GONE; 128 if (mLayout != null) { 129 lastVisibilityState = mLayout.getVisibility(); 130 mWindowManager.removeView(mLayout); 131 mLayout = null; 132 } 133 134 if (mLayoutParameter == null) { 135 initLayoutParams(); 136 } 137 138 final Display display = mService.getSystemService( 139 DisplayManager.class).getDisplay(DEFAULT_DISPLAY); 140 141 mLayout = new FrameLayout( 142 mService.createDisplayContext(display).createWindowContext( 143 WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, null)); 144 updateLayoutPosition(); 145 inflateLayoutAndSetOnTouchListener(mLayout); 146 mA11yMenuViewPager = new A11yMenuViewPager(mService); 147 mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex); 148 mWindowManager.addView(mLayout, mLayoutParameter); 149 mLayout.setVisibility(lastVisibilityState); 150 151 return mLayout; 152 } 153 154 /** Updates view layout with new layout parameters only. */ updateViewLayout()155 public void updateViewLayout() { 156 if (mLayout == null || mLayoutParameter == null) { 157 return; 158 } 159 updateLayoutPosition(); 160 mWindowManager.updateViewLayout(mLayout, mLayoutParameter); 161 } 162 initLayoutParams()163 private void initLayoutParams() { 164 mLayoutParameter = new WindowManager.LayoutParams(); 165 mLayoutParameter.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; 166 mLayoutParameter.format = PixelFormat.TRANSLUCENT; 167 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 168 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; 169 mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name)); 170 } 171 inflateLayoutAndSetOnTouchListener(ViewGroup view)172 private void inflateLayoutAndSetOnTouchListener(ViewGroup view) { 173 LayoutInflater inflater = LayoutInflater.from(mService); 174 inflater.inflate(R.layout.paged_menu, view); 175 view.setOnTouchListener(mService); 176 } 177 178 /** 179 * Loads shortcut data from default shortcut ID array. 180 * 181 * @return A list of default shortcuts 182 */ createShortcutList()183 private List<A11yMenuShortcut> createShortcutList() { 184 List<A11yMenuShortcut> shortcutList = new ArrayList<>(); 185 186 for (int shortcutId : 187 (A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService) 188 ? LARGE_SHORTCUT_LIST_DEFAULT : SHORTCUT_LIST_DEFAULT)) { 189 shortcutList.add(new A11yMenuShortcut(shortcutId)); 190 } 191 return shortcutList; 192 } 193 194 /** Updates a11y menu layout position by configuring layout params. */ updateLayoutPosition()195 private void updateLayoutPosition() { 196 final Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); 197 final Configuration configuration = mService.getResources().getConfiguration(); 198 final int orientation = configuration.orientation; 199 if (display != null && orientation == Configuration.ORIENTATION_LANDSCAPE) { 200 final boolean ltr = configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 201 switch (display.getRotation()) { 202 case Surface.ROTATION_0: 203 case Surface.ROTATION_180: 204 mLayoutParameter.gravity = 205 (ltr ? Gravity.END : Gravity.START) | Gravity.BOTTOM 206 | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; 207 mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT; 208 mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT; 209 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 210 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; 211 mLayout.setBackgroundResource(R.drawable.shadow_90deg); 212 break; 213 case Surface.ROTATION_90: 214 case Surface.ROTATION_270: 215 mLayoutParameter.gravity = 216 (ltr ? Gravity.START : Gravity.END) | Gravity.BOTTOM 217 | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; 218 mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT; 219 mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT; 220 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 221 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; 222 mLayout.setBackgroundResource(R.drawable.shadow_270deg); 223 break; 224 default: 225 break; 226 } 227 } else { 228 mLayoutParameter.gravity = Gravity.BOTTOM; 229 mLayoutParameter.width = WindowManager.LayoutParams.MATCH_PARENT; 230 mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT; 231 mLayout.setBackgroundResource(R.drawable.shadow_0deg); 232 } 233 234 // Adjusts the y position of a11y menu layout to make the layout not to overlap bottom 235 // navigation bar window. 236 updateLayoutByWindowInsetsIfNeeded(); 237 mLayout.setOnApplyWindowInsetsListener( 238 (view, insets) -> { 239 if (updateLayoutByWindowInsetsIfNeeded()) { 240 mWindowManager.updateViewLayout(mLayout, mLayoutParameter); 241 } 242 return view.onApplyWindowInsets(insets); 243 }); 244 } 245 246 /** 247 * Returns {@code true} if the a11y menu layout params 248 * should be updated by {@link WindowManager} immediately due to window insets change. 249 * This method adjusts the layout position and size to 250 * make a11y menu not to overlap navigation bar window. 251 */ updateLayoutByWindowInsetsIfNeeded()252 private boolean updateLayoutByWindowInsetsIfNeeded() { 253 boolean shouldUpdateLayout = false; 254 WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); 255 Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility( 256 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); 257 int xOffset = max(windowInsets.left, windowInsets.right); 258 int yOffset = windowInsets.bottom; 259 Rect windowBound = windowMetrics.getBounds(); 260 if (mLayoutParameter.x != xOffset || mLayoutParameter.y != yOffset) { 261 mLayoutParameter.x = xOffset; 262 mLayoutParameter.y = yOffset; 263 shouldUpdateLayout = true; 264 } 265 // for gestural navigation mode and the landscape mode, 266 // the layout height should be decreased by system bar 267 // and display cutout inset to fit the new 268 // frame size that doesn't overlap the navigation bar window. 269 int orientation = mService.getResources().getConfiguration().orientation; 270 if (mLayout.getHeight() != mLayoutParameter.height 271 && orientation == Configuration.ORIENTATION_LANDSCAPE) { 272 mLayoutParameter.height = windowBound.height() - yOffset; 273 shouldUpdateLayout = true; 274 } 275 return shouldUpdateLayout; 276 } 277 278 /** 279 * Gets the current page index when device configuration changed. {@link 280 * AccessibilityMenuService#onConfigurationChanged(Configuration)} 281 * 282 * @return the current index of the ViewPager. 283 */ getPageIndex()284 public int getPageIndex() { 285 if (mA11yMenuViewPager != null) { 286 return mA11yMenuViewPager.mViewPager.getCurrentItem(); 287 } 288 return A11yMenuViewPager.DEFAULT_PAGE_INDEX; 289 } 290 291 /** 292 * Hides a11y menu layout. And return if layout visibility has been changed. 293 * 294 * @return {@code true} layout visibility is toggled off; {@code false} is unchanged 295 */ hideMenu()296 public boolean hideMenu() { 297 if (mLayout.getVisibility() == View.VISIBLE) { 298 mLayout.setVisibility(View.GONE); 299 return true; 300 } 301 return false; 302 } 303 304 /** Toggles a11y menu layout visibility. */ toggleVisibility()305 public void toggleVisibility() { 306 mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE); 307 } 308 309 /** Shows hint text on a minimal Snackbar-like text view. */ showSnackbar(String text)310 public void showSnackbar(String text) { 311 final int animationDurationMs = 300; 312 final int timeoutDurationMs = mAccessibilityManager.getRecommendedTimeoutMillis(2000, 313 AccessibilityManager.FLAG_CONTENT_TEXT); 314 315 final TextView snackbar = mLayout.findViewById(R.id.snackbar); 316 snackbar.setText(text); 317 318 // Remove any existing fade-out animation before starting any new animations. 319 mHandler.removeCallbacksAndMessages(null); 320 321 if (snackbar.getVisibility() != View.VISIBLE) { 322 snackbar.setAlpha(0f); 323 snackbar.setVisibility(View.VISIBLE); 324 snackbar.animate().alpha(1f).setDuration(animationDurationMs).setListener(null); 325 } 326 mHandler.postDelayed(() -> snackbar.animate().alpha(0f).setDuration( 327 animationDurationMs).setListener( 328 new AnimatorListenerAdapter() { 329 @Override 330 public void onAnimationEnd(@NonNull Animator animation) { 331 snackbar.setVisibility(View.GONE); 332 } 333 }), timeoutDurationMs); 334 } 335 } 336