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