1 /* 2 * Copyright (C) 2024 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 21 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; 22 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; 23 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT; 24 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT; 25 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP; 26 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION; 27 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; 28 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; 29 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; 30 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; 31 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; 32 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; 33 34 import android.annotation.NonNull; 35 import android.content.res.Configuration; 36 import android.graphics.Rect; 37 38 /** 39 * Encapsulate overrides and configurations about app compat reachability. 40 */ 41 class AppCompatReachabilityOverrides { 42 43 @NonNull 44 private final ActivityRecord mActivityRecord; 45 @NonNull 46 private final AppCompatConfiguration mAppCompatConfiguration; 47 @NonNull 48 private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery; 49 @NonNull 50 private final ReachabilityState mReachabilityState; 51 AppCompatReachabilityOverrides(@onNull ActivityRecord activityRecord, @NonNull AppCompatConfiguration appCompatConfiguration, @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery)52 AppCompatReachabilityOverrides(@NonNull ActivityRecord activityRecord, 53 @NonNull AppCompatConfiguration appCompatConfiguration, 54 @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) { 55 mActivityRecord = activityRecord; 56 mAppCompatConfiguration = appCompatConfiguration; 57 mAppCompatDeviceStateQuery = appCompatDeviceStateQuery; 58 mReachabilityState = new ReachabilityState(); 59 } 60 isFromDoubleTap()61 boolean isFromDoubleTap() { 62 return mReachabilityState.isFromDoubleTap(); 63 } 64 isDoubleTapEvent()65 boolean isDoubleTapEvent() { 66 return mReachabilityState.mIsDoubleTapEvent; 67 } 68 setDoubleTapEvent()69 void setDoubleTapEvent() { 70 mReachabilityState.mIsDoubleTapEvent = true; 71 } 72 73 /** 74 * Provides the multiplier to use when calculating the position of a letterboxed app after 75 * an horizontal reachability event (double tap). The method takes the current state of the 76 * device (e.g. device in book mode) into account. 77 * </p> 78 * @param parentConfiguration The parent {@link Configuration}. 79 * @return The value to use for calculating the letterbox horizontal position. 80 */ getHorizontalPositionMultiplier(@onNull Configuration parentConfiguration)81 float getHorizontalPositionMultiplier(@NonNull Configuration parentConfiguration) { 82 // Don't check resolved configuration because it may not be updated yet during 83 // configuration change. 84 boolean bookModeEnabled = isFullScreenAndBookModeEnabled(); 85 return isHorizontalReachabilityEnabled(parentConfiguration) 86 // Using the last global dynamic position to avoid "jumps" when moving 87 // between apps or activities. 88 ? mAppCompatConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled) 89 : mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled); 90 } 91 92 /** 93 * Provides the multiplier to use when calculating the position of a letterboxed app after 94 * a vertical reachability event (double tap). The method takes the current state of the 95 * device (e.g. device posture) into account. 96 * </p> 97 * @param parentConfiguration The parent {@link Configuration}. 98 * @return The value to use for calculating the letterbox horizontal position. 99 */ getVerticalPositionMultiplier(@onNull Configuration parentConfiguration)100 float getVerticalPositionMultiplier(@NonNull Configuration parentConfiguration) { 101 // Don't check resolved configuration because it may not be updated yet during 102 // configuration change. 103 boolean tabletopMode = mAppCompatDeviceStateQuery 104 .isDisplayFullScreenAndInPosture(/* isTabletop */ true); 105 return isVerticalReachabilityEnabled(parentConfiguration) 106 // Using the last global dynamic position to avoid "jumps" when moving 107 // between apps or activities. 108 ? mAppCompatConfiguration.getVerticalMultiplierForReachability(tabletopMode) 109 : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode); 110 } 111 isHorizontalReachabilityEnabled()112 boolean isHorizontalReachabilityEnabled() { 113 return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); 114 } 115 isVerticalReachabilityEnabled()116 boolean isVerticalReachabilityEnabled() { 117 return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); 118 } 119 isLetterboxDoubleTapEducationEnabled()120 boolean isLetterboxDoubleTapEducationEnabled() { 121 return isHorizontalReachabilityEnabled() || isVerticalReachabilityEnabled(); 122 } 123 124 @AppCompatConfiguration.LetterboxVerticalReachabilityPosition getLetterboxPositionForVerticalReachability()125 int getLetterboxPositionForVerticalReachability() { 126 final boolean isInFullScreenTabletopMode = 127 mAppCompatDeviceStateQuery.isDisplayFullScreenAndSeparatingHinge(); 128 return mAppCompatConfiguration.getLetterboxPositionForVerticalReachability( 129 isInFullScreenTabletopMode); 130 } 131 132 @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition getLetterboxPositionForHorizontalReachability()133 int getLetterboxPositionForHorizontalReachability() { 134 final boolean isInFullScreenBookMode = isFullScreenAndBookModeEnabled(); 135 return mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability( 136 isInFullScreenBookMode); 137 } 138 getLetterboxPositionForLogging()139 int getLetterboxPositionForLogging() { 140 int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION; 141 if (isHorizontalReachabilityEnabled()) { 142 int letterboxPositionForHorizontalReachability = mAppCompatConfiguration 143 .getLetterboxPositionForHorizontalReachability(mAppCompatDeviceStateQuery 144 .isDisplayFullScreenAndInPosture(/* isTabletop */ false)); 145 positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPositionForLogging( 146 letterboxPositionForHorizontalReachability); 147 } else if (isVerticalReachabilityEnabled()) { 148 int letterboxPositionForVerticalReachability = mAppCompatConfiguration 149 .getLetterboxPositionForVerticalReachability(mAppCompatDeviceStateQuery 150 .isDisplayFullScreenAndInPosture(/* isTabletop */ true)); 151 positionToLog = letterboxVerticalReachabilityPositionToLetterboxPositionForLogging( 152 letterboxPositionForVerticalReachability); 153 } 154 return positionToLog; 155 } 156 157 /** 158 * @return {@code true} if the vertical reachability should be allowed in case of 159 * thin letterboxing. 160 */ allowVerticalReachabilityForThinLetterbox()161 boolean allowVerticalReachabilityForThinLetterbox() { 162 // When the flag is enabled we allow vertical reachability only if the 163 // app is not thin letterboxed vertically. 164 return !isVerticalThinLetterboxed(); 165 } 166 167 /** 168 * @return {@code true} if the horizontal reachability should be enabled in case of 169 * thin letterboxing. 170 */ allowHorizontalReachabilityForThinLetterbox()171 boolean allowHorizontalReachabilityForThinLetterbox() { 172 // When the flag is enabled we allow horizontal reachability only if the 173 // app is not thin pillarboxed. 174 return !isHorizontalThinLetterboxed(); 175 } 176 177 /** 178 * @return {@code true} if the resulting app is letterboxed in a way defined as thin. 179 */ isVerticalThinLetterboxed()180 boolean isVerticalThinLetterboxed() { 181 final int thinHeight = mAppCompatConfiguration.getThinLetterboxHeightPx(); 182 if (thinHeight < 0) { 183 return false; 184 } 185 final Task task = mActivityRecord.getTask(); 186 if (task == null) { 187 return false; 188 } 189 final int padding = Math.abs( 190 task.getBounds().height() - mActivityRecord.getBounds().height()) / 2; 191 return padding <= thinHeight; 192 } 193 194 /** 195 * @return {@code true} if the resulting app is pillarboxed in a way defined as thin. 196 */ isHorizontalThinLetterboxed()197 boolean isHorizontalThinLetterboxed() { 198 final int thinWidth = mAppCompatConfiguration.getThinLetterboxWidthPx(); 199 if (thinWidth < 0) { 200 return false; 201 } 202 final Task task = mActivityRecord.getTask(); 203 if (task == null) { 204 return false; 205 } 206 final int padding = Math.abs( 207 task.getBounds().width() - mActivityRecord.getBounds().width()) / 2; 208 return padding <= thinWidth; 209 } 210 211 // Note that we check the task rather than the parent as with ActivityEmbedding the parent might 212 // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is 213 // actually fullscreen. isDisplayFullScreenAndSeparatingHinge()214 private boolean isDisplayFullScreenAndSeparatingHinge() { 215 Task task = mActivityRecord.getTask(); 216 return mActivityRecord.mDisplayContent != null 217 && mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge() 218 && task != null 219 && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN; 220 } 221 letterboxHorizontalReachabilityPositionToLetterboxPositionForLogging( @ppCompatConfiguration.LetterboxHorizontalReachabilityPosition int position)222 private int letterboxHorizontalReachabilityPositionToLetterboxPositionForLogging( 223 @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition int position) { 224 switch (position) { 225 case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT: 226 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT; 227 case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER: 228 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; 229 case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT: 230 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT; 231 default: 232 throw new AssertionError( 233 "Unexpected letterbox horizontal reachability position type: " 234 + position); 235 } 236 } 237 letterboxVerticalReachabilityPositionToLetterboxPositionForLogging( @ppCompatConfiguration.LetterboxVerticalReachabilityPosition int position)238 private int letterboxVerticalReachabilityPositionToLetterboxPositionForLogging( 239 @AppCompatConfiguration.LetterboxVerticalReachabilityPosition int position) { 240 switch (position) { 241 case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP: 242 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP; 243 case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER: 244 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; 245 case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM: 246 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; 247 default: 248 throw new AssertionError( 249 "Unexpected letterbox vertical reachability position type: " 250 + position); 251 } 252 } 253 isFullScreenAndBookModeEnabled()254 private boolean isFullScreenAndBookModeEnabled() { 255 return mAppCompatDeviceStateQuery.isDisplayFullScreenAndInPosture(/* isTabletop */ false) 256 && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled(); 257 } 258 259 /** 260 * Whether horizontal reachability is enabled for an activity in the current configuration. 261 * 262 * <p>Conditions that needs to be met: 263 * <ul> 264 * <li>Windowing mode is fullscreen. 265 * <li>Horizontal Reachability is enabled. 266 * <li>First top opaque activity fills parent vertically, but not horizontally. 267 * </ul> 268 */ isHorizontalReachabilityEnabled(@onNull Configuration parentConfiguration)269 private boolean isHorizontalReachabilityEnabled(@NonNull Configuration parentConfiguration) { 270 if (!allowHorizontalReachabilityForThinLetterbox()) { 271 return false; 272 } 273 final Rect parentAppBoundsOverride = mActivityRecord.getParentAppBoundsOverride(); 274 final Rect parentAppBounds = parentAppBoundsOverride != null 275 ? parentAppBoundsOverride : parentConfiguration.windowConfiguration.getAppBounds(); 276 // Use screen resolved bounds which uses resolved bounds or size compat bounds 277 // as activity bounds can sometimes be empty 278 final Rect opaqueActivityBounds = mActivityRecord.mAppCompatController 279 .getTransparentPolicy().getFirstOpaqueActivity() 280 .map(ActivityRecord::getScreenResolvedBounds) 281 .orElse(mActivityRecord.getScreenResolvedBounds()); 282 return mAppCompatConfiguration.getIsHorizontalReachabilityEnabled() 283 && parentConfiguration.windowConfiguration.getWindowingMode() 284 == WINDOWING_MODE_FULLSCREEN 285 // Check whether the activity fills the parent vertically. 286 && parentAppBounds.height() <= opaqueActivityBounds.height() 287 && parentAppBounds.width() > opaqueActivityBounds.width(); 288 } 289 290 /** 291 * Whether vertical reachability is enabled for an activity in the current configuration. 292 * 293 * <p>Conditions that needs to be met: 294 * <ul> 295 * <li>Windowing mode is fullscreen. 296 * <li>Vertical Reachability is enabled. 297 * <li>First top opaque activity fills parent horizontally but not vertically. 298 * </ul> 299 */ isVerticalReachabilityEnabled(@onNull Configuration parentConfiguration)300 private boolean isVerticalReachabilityEnabled(@NonNull Configuration parentConfiguration) { 301 if (!allowVerticalReachabilityForThinLetterbox()) { 302 return false; 303 } 304 final Rect parentAppBoundsOverride = mActivityRecord.getParentAppBoundsOverride(); 305 final Rect parentAppBounds = parentAppBoundsOverride != null 306 ? parentAppBoundsOverride : parentConfiguration.windowConfiguration.getAppBounds(); 307 // Use screen resolved bounds which uses resolved bounds or size compat bounds 308 // as activity bounds can sometimes be empty. 309 final Rect opaqueActivityBounds = mActivityRecord.mAppCompatController 310 .getTransparentPolicy().getFirstOpaqueActivity() 311 .map(ActivityRecord::getScreenResolvedBounds) 312 .orElse(mActivityRecord.getScreenResolvedBounds()); 313 return mAppCompatConfiguration.getIsVerticalReachabilityEnabled() 314 && parentConfiguration.windowConfiguration.getWindowingMode() 315 == WINDOWING_MODE_FULLSCREEN 316 // Check whether the activity fills the parent horizontally. 317 && parentAppBounds.width() <= opaqueActivityBounds.width() 318 && parentAppBounds.height() > opaqueActivityBounds.height(); 319 } 320 321 private static class ReachabilityState { 322 // If the current event is a double tap. 323 private boolean mIsDoubleTapEvent; 324 isFromDoubleTap()325 boolean isFromDoubleTap() { 326 final boolean isFromDoubleTap = mIsDoubleTapEvent; 327 mIsDoubleTapEvent = false; 328 return isFromDoubleTap; 329 } 330 } 331 332 } 333