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