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