1 /*
2  * Copyright (C) 2022 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.server.wm;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
21 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
22 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
23 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
24 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
25 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
26 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
27 import static android.content.pm.ActivityInfo.screenOrientationToString;
28 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
29 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
30 import static android.view.Display.TYPE_INTERNAL;
31 
32 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
33 import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_CAMERA_COMPAT;
34 
35 import android.annotation.NonNull;
36 import android.annotation.Nullable;
37 import android.annotation.StringRes;
38 import android.content.pm.ActivityInfo.ScreenOrientation;
39 import android.content.pm.PackageManager;
40 import android.content.res.Configuration;
41 import android.widget.Toast;
42 
43 import com.android.internal.R;
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.protolog.common.ProtoLog;
46 import com.android.server.UiThread;
47 
48 /**
49  * Controls camera compatibility treatment that handles orientation mismatch between camera
50  * buffers and an app window for a particular display that can lead to camera issues like sideways
51  * or stretched viewfinder.
52  *
53  * <p>This includes force rotation of fixed orientation activities connected to the camera.
54  *
55  * <p>The treatment is enabled for internal displays that have {@code ignoreOrientationRequest}
56  * display setting enabled and when {@code
57  * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
58  */
59  // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path
60 final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraCompatStateListener,
61         ActivityRefresher.Evaluator {
62 
63     @NonNull
64     private final DisplayContent mDisplayContent;
65     @NonNull
66     private final WindowManagerService mWmService;
67     @NonNull
68     private final CameraStateMonitor mCameraStateMonitor;
69     @NonNull
70     private final ActivityRefresher mActivityRefresher;
71 
72     @ScreenOrientation
73     private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET;
74 
DisplayRotationCompatPolicy(@onNull DisplayContent displayContent, @NonNull CameraStateMonitor cameraStateMonitor, @NonNull ActivityRefresher activityRefresher)75     DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent,
76             @NonNull CameraStateMonitor cameraStateMonitor,
77             @NonNull ActivityRefresher activityRefresher) {
78         // This constructor is called from DisplayContent constructor. Don't use any fields in
79         // DisplayContent here since they aren't guaranteed to be set.
80         mDisplayContent = displayContent;
81         mWmService = displayContent.mWmService;
82         mCameraStateMonitor = cameraStateMonitor;
83         mActivityRefresher = activityRefresher;
84     }
85 
start()86     void start() {
87         mCameraStateMonitor.addCameraStateListener(this);
88         mActivityRefresher.addEvaluator(this);
89     }
90 
91     /** Releases camera state listener. */
dispose()92     void dispose() {
93         mCameraStateMonitor.removeCameraStateListener(this);
94         mActivityRefresher.removeEvaluator(this);
95     }
96 
97     /**
98      * Determines orientation for Camera compatibility.
99      *
100      * <p>The goal of this function is to compute a orientation which would align orientations of
101      * portrait app window and natural orientation of the device and set opposite to natural
102      * orientation for a landscape app window. This is one of the strongest assumptions that apps
103      * make when they implement camera previews. Since app and natural display orientations aren't
104      * guaranteed to match, the rotation can cause letterboxing.
105      *
106      * <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link
107      * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment.
108      */
109     @ScreenOrientation
getOrientation()110     int getOrientation() {
111         mLastReportedOrientation = getOrientationInternal();
112         if (mLastReportedOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
113             rememberOverriddenOrientationIfNeeded();
114         } else {
115             restoreOverriddenOrientationIfNeeded();
116         }
117         return mLastReportedOrientation;
118     }
119 
120     @ScreenOrientation
getOrientationInternal()121     private synchronized int getOrientationInternal() {
122         if (!isTreatmentEnabledForDisplay()) {
123             return SCREEN_ORIENTATION_UNSPECIFIED;
124         }
125         final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
126                 /* considerKeyguardState= */ true);
127         if (!isTreatmentEnabledForActivity(topActivity)) {
128             return SCREEN_ORIENTATION_UNSPECIFIED;
129         }
130         boolean isPortraitActivity =
131                 topActivity.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT;
132         boolean isNaturalDisplayOrientationPortrait =
133                 mDisplayContent.getNaturalOrientation() == ORIENTATION_PORTRAIT;
134         // Rotate portrait-only activity in the natural orientation of the displays (and in the
135         // opposite to natural orientation for landscape-only) since many apps assume that those
136         // are aligned when they compute orientation of the preview.
137         // This means that even for a landscape-only activity and a device with landscape natural
138         // orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that
139         // natural orientation = portrait window = portait camera is the main wrong assumption
140         // that apps make when they implement camera previews so landscape windows need be
141         // rotated in the orientation oposite to the natural one even if it's portrait.
142         // TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions
143         // of the portrait and landscape orientation requests.
144         final int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait)
145                 || (!isPortraitActivity && !isNaturalDisplayOrientationPortrait)
146                         ? SCREEN_ORIENTATION_PORTRAIT
147                         : SCREEN_ORIENTATION_LANDSCAPE;
148         ProtoLog.v(WM_DEBUG_ORIENTATION,
149                 "Display id=%d is ignoring all orientation requests, camera is active "
150                         + "and the top activity is eligible for force rotation, return %s,"
151                         + "portrait activity: %b, is natural orientation portrait: %b.",
152                 mDisplayContent.mDisplayId, screenOrientationToString(orientation),
153                 isPortraitActivity, isNaturalDisplayOrientationPortrait);
154         return orientation;
155     }
156 
157     /**
158      * Notifies that animation in {@link ScreenRotationAnimation} has finished.
159      *
160      * <p>This class uses this signal as a trigger for notifying the user about forced rotation
161      * reason with the {@link Toast}.
162      */
onScreenRotationAnimationFinished()163     void onScreenRotationAnimationFinished() {
164         final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
165                 /* considerKeyguardState= */ true);
166         if (!isTreatmentEnabledForDisplay()
167                 || !isTreatmentEnabledForActivity(topActivity)) {
168             return;
169         }
170         showToast(R.string.display_rotation_camera_compat_toast_after_rotation);
171     }
172 
getSummaryForDisplayRotationHistoryRecord()173     String getSummaryForDisplayRotationHistoryRecord() {
174         String summaryIfEnabled = "";
175         if (isTreatmentEnabledForDisplay()) {
176             ActivityRecord topActivity = mDisplayContent.topRunningActivity(
177                     /* considerKeyguardState= */ true);
178             summaryIfEnabled =
179                     " mLastReportedOrientation="
180                             + screenOrientationToString(mLastReportedOrientation)
181                     + " topActivity="
182                             + (topActivity == null ? "null" : topActivity.shortComponentName)
183                     + " isTreatmentEnabledForActivity="
184                             + isTreatmentEnabledForActivity(topActivity)
185                             + "mCameraStateMonitor="
186                             + mCameraStateMonitor.getSummary();
187         }
188         return "DisplayRotationCompatPolicy{"
189                 + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay()
190                 + summaryIfEnabled
191                 + " }";
192     }
193 
restoreOverriddenOrientationIfNeeded()194     private void restoreOverriddenOrientationIfNeeded() {
195         if (!isOrientationOverridden()) {
196             return;
197         }
198         if (mDisplayContent.getRotationReversionController().revertOverride(
199                 REVERSION_TYPE_CAMERA_COMPAT)) {
200             ProtoLog.v(WM_DEBUG_ORIENTATION,
201                     "Reverting orientation after camera compat force rotation");
202             // Reset last orientation source since we have reverted the orientation.
203             mDisplayContent.mLastOrientationSource = null;
204         }
205     }
206 
isOrientationOverridden()207     private boolean isOrientationOverridden() {
208         return mDisplayContent.getRotationReversionController().isOverrideActive(
209                 REVERSION_TYPE_CAMERA_COMPAT);
210     }
211 
rememberOverriddenOrientationIfNeeded()212     private void rememberOverriddenOrientationIfNeeded() {
213         if (!isOrientationOverridden()) {
214             mDisplayContent.getRotationReversionController().beforeOverrideApplied(
215                     REVERSION_TYPE_CAMERA_COMPAT);
216             ProtoLog.v(WM_DEBUG_ORIENTATION,
217                     "Saving original orientation before camera compat, last orientation is %d",
218                     mDisplayContent.getLastOrientation());
219         }
220     }
221 
222     // Refreshing only when configuration changes after rotation or camera split screen aspect ratio
223     // treatment is enabled
224     @Override
shouldRefreshActivity(@onNull ActivityRecord activity, @NonNull Configuration newConfig, @NonNull Configuration lastReportedConfig)225     public boolean shouldRefreshActivity(@NonNull ActivityRecord activity,
226             @NonNull Configuration newConfig, @NonNull Configuration lastReportedConfig) {
227         final boolean displayRotationChanged = (newConfig.windowConfiguration.getDisplayRotation()
228                 != lastReportedConfig.windowConfiguration.getDisplayRotation());
229         return isTreatmentEnabledForDisplay()
230                 && isTreatmentEnabledForActivity(activity)
231                 && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat()
232                 && (displayRotationChanged
233                 || activity.mLetterboxUiController.isCameraCompatSplitScreenAspectRatioAllowed());
234     }
235 
236     /**
237      * Whether camera compat treatment is enabled for the display.
238      *
239      * <p>Conditions that need to be met:
240      * <ul>
241      *     <li>{@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
242      *     <li>Setting {@code ignoreOrientationRequest} is enabled for the display.
243      *     <li>Associated {@link DisplayContent} is for internal display. See b/225928882
244      *     that tracks supporting external displays in the future.
245      * </ul>
246      */
isTreatmentEnabledForDisplay()247     private boolean isTreatmentEnabledForDisplay() {
248         return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled()
249                 && mDisplayContent.getIgnoreOrientationRequest()
250                 // TODO(b/225928882): Support camera compat rotation for external displays
251                 && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
252     }
253 
isActivityEligibleForOrientationOverride(@onNull ActivityRecord activity)254     boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
255         return isTreatmentEnabledForDisplay()
256                 && isCameraActive(activity, /* mustBeFullscreen */ true)
257                 && activity.mLetterboxUiController.shouldForceRotateForCameraCompat();
258     }
259 
260     /**
261      * Whether camera compat treatment is applicable for the given activity.
262      *
263      * <p>Conditions that need to be met:
264      * <ul>
265      *     <li>Camera is active for the package.
266      *     <li>The activity is in fullscreen
267      *     <li>The activity has fixed orientation but not "locked" or "nosensor" one.
268      * </ul>
269      */
isTreatmentEnabledForActivity(@ullable ActivityRecord activity)270     boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
271         return isTreatmentEnabledForActivity(activity, /* mustBeFullscreen */ true);
272     }
273 
isCameraActive(@onNull ActivityRecord activity, boolean mustBeFullscreen)274     boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) {
275         // Checking windowing mode on activity level because we don't want to
276         // apply treatment in case of activity embedding.
277         return (!mustBeFullscreen || !activity.inMultiWindowMode())
278                 && mCameraStateMonitor.isCameraRunningForActivity(activity);
279     }
280 
isTreatmentEnabledForActivity(@ullable ActivityRecord activity, boolean mustBeFullscreen)281     private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity,
282             boolean mustBeFullscreen) {
283         return activity != null && isCameraActive(activity, mustBeFullscreen)
284                 && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
285                 // "locked" and "nosensor" values are often used by camera apps that can't
286                 // handle dynamic changes so we shouldn't force rotate them.
287                 && activity.getOverrideOrientation() != SCREEN_ORIENTATION_NOSENSOR
288                 && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED
289                 && activity.mLetterboxUiController.shouldForceRotateForCameraCompat();
290     }
291 
292     @Override
onCameraOpened(@onNull ActivityRecord cameraActivity, @NonNull String cameraId)293     public boolean onCameraOpened(@NonNull ActivityRecord cameraActivity,
294             @NonNull String cameraId) {
295         // Checking whether an activity in fullscreen rather than the task as this camera
296         // compat treatment doesn't cover activity embedding.
297         if (cameraActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
298             cameraActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded();
299             mDisplayContent.updateOrientation();
300             return true;
301         }
302         // Checking that the whole app is in multi-window mode as we shouldn't show toast
303         // for the activity embedding case.
304         if (cameraActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
305                 && isTreatmentEnabledForActivity(
306                 cameraActivity, /* mustBeFullscreen */ false)) {
307             final PackageManager packageManager = mWmService.mContext.getPackageManager();
308             try {
309                 showToast(
310                         R.string.display_rotation_camera_compat_toast_in_multi_window,
311                         (String) packageManager.getApplicationLabel(
312                                 packageManager.getApplicationInfo(cameraActivity.packageName,
313                                         /* flags */ 0)));
314                 return true;
315             } catch (PackageManager.NameNotFoundException e) {
316                 ProtoLog.e(WM_DEBUG_ORIENTATION,
317                         "DisplayRotationCompatPolicy: Multi-window toast not shown as "
318                                 + "package '%s' cannot be found.",
319                         cameraActivity.packageName);
320             }
321         }
322         return false;
323     }
324 
325     @VisibleForTesting
showToast(@tringRes int stringRes)326     void showToast(@StringRes int stringRes) {
327         UiThread.getHandler().post(
328                 () -> Toast.makeText(mWmService.mContext, stringRes, Toast.LENGTH_LONG).show());
329     }
330 
331     @VisibleForTesting
showToast(@tringRes int stringRes, @NonNull String applicationLabel)332     void showToast(@StringRes int stringRes, @NonNull String applicationLabel) {
333         UiThread.getHandler().post(
334                 () -> Toast.makeText(
335                         mWmService.mContext,
336                         mWmService.mContext.getString(stringRes, applicationLabel),
337                         Toast.LENGTH_LONG).show());
338     }
339 
340     @Override
onCameraClosed(@onNull ActivityRecord cameraActivity, @NonNull String cameraId)341     public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity,
342             @NonNull String cameraId) {
343         synchronized (this) {
344             // TODO(b/336474959): Once refresh is implemented in `CameraCompatFreeformPolicy`,
345             // consider checking this in CameraStateMonitor before notifying the listeners (this).
346             if (isActivityForCameraIdRefreshing(cameraId)) {
347                 ProtoLog.v(WM_DEBUG_ORIENTATION,
348                         "Display id=%d is notified that camera is closed but activity is"
349                                 + " still refreshing. Rescheduling an update.",
350                         mDisplayContent.mDisplayId);
351                 return false;
352             }
353         }
354         ProtoLog.v(WM_DEBUG_ORIENTATION,
355                 "Display id=%d is notified that Camera is closed, updating rotation.",
356                 mDisplayContent.mDisplayId);
357         final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
358                 /* considerKeyguardState= */ true);
359         if (topActivity == null
360                 // Checking whether an activity in fullscreen rather than the task as this
361                 // camera compat treatment doesn't cover activity embedding.
362                 || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
363             return true;
364         }
365         topActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded();
366         mDisplayContent.updateOrientation();
367         return true;
368     }
369 
370     // TODO(b/336474959): Do we need cameraId here?
isActivityForCameraIdRefreshing(@onNull String cameraId)371     private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) {
372         final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
373                 /* considerKeyguardState= */ true);
374         if (!isTreatmentEnabledForActivity(topActivity)
375                 || !mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
376             return false;
377         }
378         return mActivityRefresher.isActivityRefreshing(topActivity);
379     }
380 }
381