1 /* 2 * Copyright (C) 2021 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.wm.shell.compatui; 18 19 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; 20 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; 21 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; 22 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; 23 import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; 24 import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; 25 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.app.CameraCompatTaskInfo.CameraCompatControlState; 29 import android.app.TaskInfo; 30 import android.content.Context; 31 import android.graphics.Rect; 32 import android.util.Log; 33 import android.util.Pair; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.window.flags.Flags; 39 import com.android.wm.shell.R; 40 import com.android.wm.shell.ShellTaskOrganizer; 41 import com.android.wm.shell.common.DisplayLayout; 42 import com.android.wm.shell.common.SyncTransactionQueue; 43 import com.android.wm.shell.compatui.CompatUIController.CompatUICallback; 44 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; 45 46 import java.util.function.Consumer; 47 48 /** 49 * Window manager for the Size Compat restart button and Camera Compat control. 50 */ 51 class CompatUIWindowManager extends CompatUIWindowManagerAbstract { 52 53 private final CompatUICallback mCallback; 54 55 private final CompatUIConfiguration mCompatUIConfiguration; 56 57 private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked; 58 59 // Remember the last reported states in case visibility changes due to keyguard or IME updates. 60 @VisibleForTesting 61 boolean mHasSizeCompat; 62 63 @VisibleForTesting 64 @CameraCompatControlState 65 int mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN; 66 67 @VisibleForTesting 68 CompatUIHintsState mCompatUIHintsState; 69 70 @Nullable 71 @VisibleForTesting 72 CompatUILayout mLayout; 73 74 private final float mHideScmTolerance; 75 CompatUIWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, CompatUICallback callback, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, CompatUIHintsState compatUIHintsState, CompatUIConfiguration compatUIConfiguration, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked)76 CompatUIWindowManager(Context context, TaskInfo taskInfo, 77 SyncTransactionQueue syncQueue, CompatUICallback callback, 78 ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, 79 CompatUIHintsState compatUIHintsState, CompatUIConfiguration compatUIConfiguration, 80 Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) { 81 super(context, taskInfo, syncQueue, taskListener, displayLayout); 82 mCallback = callback; 83 mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; 84 if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) { 85 // Don't show the SCM button for freeform tasks 86 mHasSizeCompat &= !taskInfo.isFreeform(); 87 } 88 mCameraCompatControlState = 89 taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState; 90 mCompatUIHintsState = compatUIHintsState; 91 mCompatUIConfiguration = compatUIConfiguration; 92 mOnRestartButtonClicked = onRestartButtonClicked; 93 mHideScmTolerance = mCompatUIConfiguration.getHideSizeCompatRestartButtonTolerance(); 94 } 95 96 @Override getZOrder()97 protected int getZOrder() { 98 return TASK_CHILD_LAYER_COMPAT_UI + 1; 99 } 100 101 @Override getLayout()102 protected @Nullable View getLayout() { 103 return mLayout; 104 } 105 106 @Override removeLayout()107 protected void removeLayout() { 108 mLayout = null; 109 } 110 111 @Override eligibleToShowLayout()112 protected boolean eligibleToShowLayout() { 113 return (mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo())) 114 || shouldShowCameraControl(); 115 } 116 117 @Override createLayout()118 protected View createLayout() { 119 mLayout = inflateLayout(); 120 mLayout.inject(this); 121 122 updateVisibilityOfViews(); 123 124 if (mHasSizeCompat) { 125 mCallback.onSizeCompatRestartButtonAppeared(mTaskId); 126 } 127 128 return mLayout; 129 } 130 131 @VisibleForTesting inflateLayout()132 CompatUILayout inflateLayout() { 133 return (CompatUILayout) LayoutInflater.from(mContext).inflate(R.layout.compat_ui_layout, 134 null); 135 } 136 137 @Override updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow)138 public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, 139 boolean canShow) { 140 final boolean prevHasSizeCompat = mHasSizeCompat; 141 final int prevCameraCompatControlState = mCameraCompatControlState; 142 mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; 143 if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) { 144 // Don't show the SCM button for freeform tasks 145 mHasSizeCompat &= !taskInfo.isFreeform(); 146 } 147 mCameraCompatControlState = 148 taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState; 149 150 if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { 151 return false; 152 } 153 154 if (prevHasSizeCompat != mHasSizeCompat 155 || prevCameraCompatControlState != mCameraCompatControlState) { 156 updateVisibilityOfViews(); 157 } 158 159 return true; 160 } 161 162 /** Called when the restart button is clicked. */ onRestartButtonClicked()163 void onRestartButtonClicked() { 164 mOnRestartButtonClicked.accept(Pair.create(getLastTaskInfo(), getTaskListener())); 165 } 166 167 /** Called when the camera treatment button is clicked. */ onCameraTreatmentButtonClicked()168 void onCameraTreatmentButtonClicked() { 169 if (!shouldShowCameraControl()) { 170 Log.w(getTag(), "Camera compat shouldn't receive clicks in the hidden state."); 171 return; 172 } 173 // When a camera control is shown, only two states are allowed: "treament applied" and 174 // "treatment suggested". Clicks on the conrol's treatment button toggle between these 175 // two states. 176 mCameraCompatControlState = 177 mCameraCompatControlState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED 178 ? CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED 179 : CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; 180 mCallback.onCameraControlStateUpdated(mTaskId, mCameraCompatControlState); 181 mLayout.updateCameraTreatmentButton(mCameraCompatControlState); 182 } 183 184 /** Called when the camera dismiss button is clicked. */ onCameraDismissButtonClicked()185 void onCameraDismissButtonClicked() { 186 if (!shouldShowCameraControl()) { 187 Log.w(getTag(), "Camera compat shouldn't receive clicks in the hidden state."); 188 return; 189 } 190 mCameraCompatControlState = CAMERA_COMPAT_CONTROL_DISMISSED; 191 mCallback.onCameraControlStateUpdated(mTaskId, CAMERA_COMPAT_CONTROL_DISMISSED); 192 mLayout.setCameraControlVisibility(/* show= */ false); 193 } 194 195 /** Called when the restart button is long clicked. */ onRestartButtonLongClicked()196 void onRestartButtonLongClicked() { 197 if (mLayout == null) { 198 return; 199 } 200 mLayout.setSizeCompatHintVisibility(/* show= */ true); 201 } 202 203 /** Called when either dismiss or treatment camera buttons is long clicked. */ onCameraButtonLongClicked()204 void onCameraButtonLongClicked() { 205 if (mLayout == null) { 206 return; 207 } 208 mLayout.setCameraCompatHintVisibility(/* show= */ true); 209 } 210 211 @Override 212 @VisibleForTesting updateSurfacePosition()213 public void updateSurfacePosition() { 214 if (mLayout == null) { 215 return; 216 } 217 // Position of the button in the container coordinate. 218 final Rect taskBounds = getTaskBounds(); 219 final Rect taskStableBounds = getTaskStableBounds(); 220 final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL 221 ? taskStableBounds.left - taskBounds.left 222 : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth(); 223 final int positionY = taskStableBounds.bottom - taskBounds.top 224 - mLayout.getMeasuredHeight(); 225 updateSurfacePosition(positionX, positionY); 226 } 227 228 @VisibleForTesting shouldShowSizeCompatRestartButton(@onNull TaskInfo taskInfo)229 boolean shouldShowSizeCompatRestartButton(@NonNull TaskInfo taskInfo) { 230 // Always show button if display is phone sized. 231 if (!Flags.allowHideScmButton() || taskInfo.configuration.smallestScreenWidthDp 232 < LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP) { 233 return true; 234 } 235 236 final int letterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth; 237 final int letterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight; 238 final Rect stableBounds = getTaskStableBounds(); 239 final int appWidth = stableBounds.width(); 240 final int appHeight = stableBounds.height(); 241 // App is floating, should always show restart button. 242 if (appWidth > letterboxWidth && appHeight > letterboxHeight) { 243 return true; 244 } 245 // If app fills the width of the display, don't show restart button (for landscape apps) 246 // if device has a custom tolerance value. 247 if (mHideScmTolerance != mCompatUIConfiguration.getDefaultHideRestartButtonTolerance() 248 && appWidth == letterboxWidth) { 249 return false; 250 } 251 252 final int letterboxArea = letterboxWidth * letterboxHeight; 253 final int taskArea = appWidth * appHeight; 254 if (letterboxArea == 0 || taskArea == 0) { 255 return false; 256 } 257 final float percentageAreaOfLetterboxInTask = (float) letterboxArea / taskArea * 100; 258 return percentageAreaOfLetterboxInTask < mHideScmTolerance; 259 } 260 updateVisibilityOfViews()261 private void updateVisibilityOfViews() { 262 if (mLayout == null) { 263 return; 264 } 265 // Size Compat mode restart button. 266 mLayout.setRestartButtonVisibility(mHasSizeCompat); 267 // Only show by default for the first time. 268 if (mHasSizeCompat && !mCompatUIHintsState.mHasShownSizeCompatHint) { 269 mLayout.setSizeCompatHintVisibility(/* show= */ true); 270 mCompatUIHintsState.mHasShownSizeCompatHint = true; 271 } 272 273 // Camera control for stretched issues. 274 mLayout.setCameraControlVisibility(shouldShowCameraControl()); 275 // Only show by default for the first time. 276 if (shouldShowCameraControl() && !mCompatUIHintsState.mHasShownCameraCompatHint) { 277 mLayout.setCameraCompatHintVisibility(/* show= */ true); 278 mCompatUIHintsState.mHasShownCameraCompatHint = true; 279 } 280 if (shouldShowCameraControl()) { 281 mLayout.updateCameraTreatmentButton(mCameraCompatControlState); 282 } 283 } 284 shouldShowCameraControl()285 private boolean shouldShowCameraControl() { 286 return mCameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN 287 && mCameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED; 288 } 289 } 290