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.os.UserManager.DISALLOW_ADJUST_VOLUME; 20 import static android.os.UserManager.DISALLOW_CONFIG_BRIGHTNESS; 21 import static android.view.Display.DEFAULT_DISPLAY; 22 import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE; 23 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; 24 25 import static java.lang.Math.max; 26 27 import android.animation.Animator; 28 import android.animation.AnimatorListenerAdapter; 29 import android.annotation.SuppressLint; 30 import android.content.Context; 31 import android.content.res.Configuration; 32 import android.graphics.Insets; 33 import android.graphics.PixelFormat; 34 import android.graphics.Rect; 35 import android.hardware.display.DisplayManager; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.os.UserHandle; 39 import android.os.UserManager; 40 import android.view.Display; 41 import android.view.Gravity; 42 import android.view.LayoutInflater; 43 import android.view.Surface; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.view.WindowInsets; 47 import android.view.WindowManager; 48 import android.view.WindowMetrics; 49 import android.view.accessibility.AccessibilityManager; 50 import android.widget.FrameLayout; 51 import android.widget.TextView; 52 53 import androidx.annotation.NonNull; 54 import androidx.annotation.UiContext; 55 56 import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; 57 import com.android.systemui.accessibility.accessibilitymenu.R; 58 import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment; 59 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; 60 import com.android.systemui.utils.windowmanager.WindowManagerUtils; 61 62 import java.util.ArrayList; 63 import java.util.List; 64 65 /** 66 * Provides functionality for Accessibility menu layout in a11y menu overlay. There are functions to 67 * configure or update Accessibility menu layout when orientation and display size changed, and 68 * functions to toggle menu visibility when button clicked or screen off. 69 */ 70 public class A11yMenuOverlayLayout { 71 72 /** Predefined default shortcuts when large button setting is off. */ 73 private static final int[] SHORTCUT_LIST_DEFAULT = { 74 A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(), 75 A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(), 76 A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(), 77 A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(), 78 A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(), 79 A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(), 80 A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(), 81 A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(), 82 A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(), 83 A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(), 84 A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(), 85 A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal() 86 }; 87 88 /** Predefined default shortcuts when large button setting is on. */ 89 private static final int[] LARGE_SHORTCUT_LIST_DEFAULT = { 90 A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(), 91 A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(), 92 A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(), 93 A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(), 94 A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(), 95 A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(), 96 A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(), 97 A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(), 98 A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(), 99 A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(), 100 A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(), 101 A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal() 102 }; 103 104 private final AccessibilityMenuService mService; 105 private final DisplayManager mDisplayManager; 106 private ViewGroup mLayout; 107 private WindowManager.LayoutParams mLayoutParameter; 108 private A11yMenuViewPager mA11yMenuViewPager; 109 private Handler mHandler; 110 private AccessibilityManager mAccessibilityManager; 111 A11yMenuOverlayLayout(AccessibilityMenuService service)112 public A11yMenuOverlayLayout(AccessibilityMenuService service) { 113 mService = service; 114 mDisplayManager = mService.getSystemService(DisplayManager.class); 115 configureLayout(); 116 mHandler = new Handler(Looper.getMainLooper()); 117 mAccessibilityManager = mService.getSystemService(AccessibilityManager.class); 118 } 119 120 /** Creates Accessibility menu layout and configure layout parameters. */ configureLayout()121 public View configureLayout() { 122 return configureLayout(A11yMenuViewPager.DEFAULT_PAGE_INDEX); 123 } 124 125 // TODO(b/78292783): Find a better way to inflate layout in the test. 126 /** 127 * Creates Accessibility menu layout, configure layout parameters and apply index to ViewPager. 128 * 129 * @param pageIndex the index of the ViewPager to show. 130 */ configureLayout(int pageIndex)131 public View configureLayout(int pageIndex) { 132 133 int lastVisibilityState = View.GONE; 134 if (mLayout != null) { 135 lastVisibilityState = mLayout.getVisibility(); 136 clearLayout(); 137 } 138 139 if (mLayoutParameter == null) { 140 initLayoutParams(); 141 } 142 143 final Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY); 144 final Context uiContext = mService.createWindowContext( 145 display, TYPE_ACCESSIBILITY_OVERLAY, /* options= */null); 146 final WindowManager windowManager = WindowManagerUtils.getWindowManager(uiContext); 147 mLayout = new A11yMenuFrameLayout(uiContext); 148 updateLayoutPosition(uiContext); 149 inflateLayoutAndSetOnTouchListener(mLayout, uiContext); 150 mA11yMenuViewPager = new A11yMenuViewPager(mService); 151 mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex); 152 windowManager.addView(mLayout, mLayoutParameter); 153 mLayout.setVisibility(lastVisibilityState); 154 mA11yMenuViewPager.updateFooterState(); 155 156 return mLayout; 157 } 158 clearLayout()159 public void clearLayout() { 160 if (mLayout != null) { 161 WindowManager windowManager = WindowManagerUtils.getWindowManager(mLayout.getContext()); 162 if (windowManager != null) { 163 windowManager.removeView(mLayout); 164 } 165 mLayout.setOnTouchListener(null); 166 mLayout = null; 167 } 168 } 169 170 /** Updates view layout with new layout parameters only. */ updateViewLayout()171 public void updateViewLayout() { 172 if (mLayout == null || mLayoutParameter == null) { 173 return; 174 } 175 updateLayoutPosition(mLayout.getContext()); 176 WindowManager windowManager = WindowManagerUtils.getWindowManager(mLayout.getContext()); 177 if (windowManager != null) { 178 windowManager.updateViewLayout(mLayout, mLayoutParameter); 179 } 180 } 181 initLayoutParams()182 private void initLayoutParams() { 183 mLayoutParameter = new WindowManager.LayoutParams(); 184 mLayoutParameter.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; 185 mLayoutParameter.format = PixelFormat.TRANSLUCENT; 186 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 187 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; 188 mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name)); 189 } 190 inflateLayoutAndSetOnTouchListener(ViewGroup view, @UiContext Context uiContext)191 private void inflateLayoutAndSetOnTouchListener(ViewGroup view, @UiContext Context uiContext) { 192 LayoutInflater inflater = LayoutInflater.from(uiContext); 193 inflater.inflate(R.layout.paged_menu, view); 194 view.setOnTouchListener(mService); 195 } 196 197 /** 198 * Loads shortcut data from default shortcut ID array. 199 * 200 * @return A list of default shortcuts 201 */ createShortcutList()202 private List<A11yMenuShortcut> createShortcutList() { 203 List<A11yMenuShortcut> shortcutList = new ArrayList<>(); 204 205 for (int shortcutId : 206 (A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService) 207 ? LARGE_SHORTCUT_LIST_DEFAULT : SHORTCUT_LIST_DEFAULT)) { 208 if (!isShortcutRestricted(shortcutId)) { 209 shortcutList.add(new A11yMenuShortcut(shortcutId)); 210 } 211 } 212 return shortcutList; 213 } 214 215 @SuppressLint("MissingPermission") isShortcutRestricted(int shortcutId)216 private boolean isShortcutRestricted(int shortcutId) { 217 final UserManager userManager = mService.getSystemService(UserManager.class); 218 if (userManager == null) { 219 return false; 220 } 221 final int userId = mService.getUserId(); 222 final UserHandle userHandle = UserHandle.of(userId); 223 if (shortcutId == A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal() 224 || shortcutId == A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal()) { 225 if (userManager.hasUserRestriction(DISALLOW_CONFIG_BRIGHTNESS) 226 || (com.android.systemui.Flags.enforceBrightnessBaseUserRestriction() 227 && userManager.hasBaseUserRestriction( 228 DISALLOW_CONFIG_BRIGHTNESS, userHandle))) { 229 return true; 230 } 231 } 232 if (shortcutId == A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal() 233 || shortcutId == A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal()) { 234 if (userManager.hasUserRestriction(DISALLOW_ADJUST_VOLUME) 235 || userManager.hasBaseUserRestriction(DISALLOW_ADJUST_VOLUME, userHandle)) { 236 return true; 237 } 238 } 239 return false; 240 } 241 242 /** Updates a11y menu layout position by configuring layout params. */ updateLayoutPosition(@iContext @onNull Context uiContext)243 private void updateLayoutPosition(@UiContext @NonNull Context uiContext) { 244 WindowManager windowManager = uiContext.getSystemService(WindowManager.class); 245 if (windowManager == null) { 246 return; 247 } 248 final Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); 249 final Configuration configuration = mService.getResources().getConfiguration(); 250 final int orientation = configuration.orientation; 251 if (display != null && orientation == Configuration.ORIENTATION_LANDSCAPE) { 252 final boolean ltr = configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 253 switch (display.getRotation()) { 254 case Surface.ROTATION_0: 255 case Surface.ROTATION_180: 256 mLayoutParameter.gravity = 257 (ltr ? Gravity.END : Gravity.START) | Gravity.BOTTOM 258 | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; 259 mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT; 260 mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT; 261 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 262 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; 263 mLayout.setBackgroundResource(R.drawable.shadow_90deg); 264 break; 265 case Surface.ROTATION_90: 266 case Surface.ROTATION_270: 267 mLayoutParameter.gravity = 268 (ltr ? Gravity.START : Gravity.END) | Gravity.BOTTOM 269 | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; 270 mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT; 271 mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT; 272 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 273 mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; 274 mLayout.setBackgroundResource(R.drawable.shadow_270deg); 275 break; 276 default: 277 break; 278 } 279 } else { 280 mLayoutParameter.gravity = Gravity.BOTTOM; 281 mLayoutParameter.width = WindowManager.LayoutParams.MATCH_PARENT; 282 mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT; 283 mLayout.setBackgroundResource(R.drawable.shadow_0deg); 284 } 285 // Adjusts the y position of a11y menu layout to make the layout not to overlap bottom 286 // navigation bar window. 287 updateLayoutByWindowInsetsIfNeeded(windowManager); 288 mLayout.setOnApplyWindowInsetsListener( 289 (view, insets) -> { 290 if (updateLayoutByWindowInsetsIfNeeded(windowManager)) { 291 windowManager.updateViewLayout(mLayout, mLayoutParameter); 292 } 293 return view.onApplyWindowInsets(insets); 294 }); 295 } 296 297 /** 298 * Returns {@code true} if the a11y menu layout params 299 * should be updated by {@link WindowManager} immediately due to window insets change. 300 * This method adjusts the layout position and size to 301 * make a11y menu not to overlap navigation bar window. 302 */ updateLayoutByWindowInsetsIfNeeded(@onNull WindowManager windowManager)303 private boolean updateLayoutByWindowInsetsIfNeeded(@NonNull WindowManager windowManager) { 304 boolean shouldUpdateLayout = false; 305 WindowMetrics windowMetrics = windowManager.getCurrentWindowMetrics(); 306 Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility( 307 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); 308 int xOffset = max(windowInsets.left, windowInsets.right); 309 int yOffset = windowInsets.bottom; 310 Rect windowBound = windowMetrics.getBounds(); 311 if (mLayoutParameter.x != xOffset || mLayoutParameter.y != yOffset) { 312 mLayoutParameter.x = xOffset; 313 mLayoutParameter.y = yOffset; 314 shouldUpdateLayout = true; 315 } 316 // for gestural navigation mode and the landscape mode, 317 // the layout height should be decreased by system bar 318 // and display cutout inset to fit the new 319 // frame size that doesn't overlap the navigation bar window. 320 int orientation = mService.getResources().getConfiguration().orientation; 321 if (mLayout.getHeight() != mLayoutParameter.height 322 && orientation == Configuration.ORIENTATION_LANDSCAPE) { 323 mLayoutParameter.height = windowBound.height() - yOffset; 324 shouldUpdateLayout = true; 325 } 326 return shouldUpdateLayout; 327 } 328 329 /** 330 * Gets the current page index when device configuration changed. {@link 331 * AccessibilityMenuService#onConfigurationChanged(Configuration)} 332 * 333 * @return the current index of the ViewPager. 334 */ getPageIndex()335 public int getPageIndex() { 336 if (mA11yMenuViewPager != null) { 337 return mA11yMenuViewPager.mViewPager.getCurrentItem(); 338 } 339 return A11yMenuViewPager.DEFAULT_PAGE_INDEX; 340 } 341 342 /** 343 * Hides a11y menu layout. And return if layout visibility has been changed. 344 * 345 * @return {@code true} layout visibility is toggled off; {@code false} is unchanged 346 */ hideMenu()347 public boolean hideMenu() { 348 if (mLayout.getVisibility() == View.VISIBLE) { 349 mLayout.setVisibility(View.GONE); 350 return true; 351 } 352 return false; 353 } 354 355 /** Toggles a11y menu layout visibility. */ toggleVisibility()356 public void toggleVisibility() { 357 if (mLayout.getVisibility() == View.VISIBLE) { 358 mLayout.setVisibility(View.GONE); 359 } else { 360 // Reconfigure the shortcut list in case the set of restricted actions has changed. 361 mA11yMenuViewPager.configureViewPagerAndFooter( 362 mLayout, createShortcutList(), getPageIndex()); 363 updateViewLayout(); 364 365 mLayout.setVisibility(View.VISIBLE); 366 } 367 } 368 369 /** Shows hint text on a minimal Snackbar-like text view. */ showSnackbar(String text)370 public void showSnackbar(String text) { 371 final int animationDurationMs = 300; 372 final int timeoutDurationMs = mAccessibilityManager.getRecommendedTimeoutMillis(2000, 373 AccessibilityManager.FLAG_CONTENT_TEXT); 374 375 final TextView snackbar = mLayout.findViewById(R.id.snackbar); 376 if (snackbar == null) { 377 return; 378 } 379 snackbar.setText(text); 380 snackbar.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE); 381 382 // Remove any existing fade-out animation before starting any new animations. 383 mHandler.removeCallbacksAndMessages(null); 384 385 if (snackbar.getVisibility() != View.VISIBLE) { 386 snackbar.setAlpha(0f); 387 snackbar.setVisibility(View.VISIBLE); 388 snackbar.animate().alpha(1f).setDuration(animationDurationMs).setListener(null); 389 } 390 mHandler.postDelayed(() -> snackbar.animate().alpha(0f).setDuration( 391 animationDurationMs).setListener( 392 new AnimatorListenerAdapter() { 393 @Override 394 public void onAnimationEnd(@NonNull Animator animation) { 395 snackbar.setVisibility(View.GONE); 396 } 397 }), timeoutDurationMs); 398 } 399 400 private class A11yMenuFrameLayout extends FrameLayout { A11yMenuFrameLayout(@iContext @onNull Context context)401 A11yMenuFrameLayout(@UiContext @NonNull Context context) { 402 super(context); 403 } 404 405 @Override dispatchConfigurationChanged(Configuration newConfig)406 public void dispatchConfigurationChanged(Configuration newConfig) { 407 super.dispatchConfigurationChanged(newConfig); 408 mA11yMenuViewPager.mA11yMenuFooter.updateRightToLeftDirection(newConfig); 409 } 410 } 411 } 412