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.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; 20 import static android.content.res.Configuration.ORIENTATION_UNDEFINED; 21 import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; 22 import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; 23 import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; 24 25 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN; 26 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; 27 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; 28 29 import android.annotation.NonNull; 30 import android.annotation.Nullable; 31 import android.content.res.Configuration; 32 import android.graphics.Rect; 33 34 import java.util.ArrayList; 35 import java.util.List; 36 import java.util.Optional; 37 import java.util.function.BooleanSupplier; 38 import java.util.function.Consumer; 39 import java.util.function.Predicate; 40 41 /** 42 * Encapsulate logic about translucent activities. 43 * <p/> 44 * An activity is defined as translucent if {@link ActivityRecord#fillsParent()} returns 45 * {@code false}. When the policy is running for a letterboxed activity, a transparent activity 46 * will inherit constraints about bounds, aspect ratios and orientation from the first not finishing 47 * activity below. 48 */ 49 class TransparentPolicy { 50 51 private static final String TAG = TAG_WITH_CLASS_NAME ? "TransparentPolicy" : TAG_ATM; 52 53 // The predicate used to find the first opaque not finishing activity below the potential 54 // transparent activity. 55 private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE = 56 ActivityRecord::occludesParent; 57 58 // The ActivityRecord this policy relates to. 59 @NonNull 60 private final ActivityRecord mActivityRecord; 61 62 // If transparent activity policy is enabled. 63 @NonNull 64 private final BooleanSupplier mIsTranslucentLetterboxingEnabledSupplier; 65 66 // The list of observers for the destroy event of candidate opaque activities 67 // when dealing with translucent activities. 68 @NonNull 69 private final List<TransparentPolicy> mDestroyListeners = new ArrayList<>(); 70 71 // The current state for the possible transparent activity 72 @NonNull 73 private final TransparentPolicyState mTransparentPolicyState; 74 TransparentPolicy(@onNull ActivityRecord activityRecord, @NonNull LetterboxConfiguration letterboxConfiguration)75 TransparentPolicy(@NonNull ActivityRecord activityRecord, 76 @NonNull LetterboxConfiguration letterboxConfiguration) { 77 mActivityRecord = activityRecord; 78 mIsTranslucentLetterboxingEnabledSupplier = 79 letterboxConfiguration::isTranslucentLetterboxingEnabled; 80 mTransparentPolicyState = new TransparentPolicyState(activityRecord); 81 } 82 83 /** 84 * Handles translucent activities letterboxing inheriting constraints from the 85 * first opaque activity beneath. 86 */ start()87 void start() { 88 if (!mIsTranslucentLetterboxingEnabledSupplier.getAsBoolean()) { 89 return; 90 } 91 final WindowContainer<?> parent = mActivityRecord.getParent(); 92 if (parent == null) { 93 return; 94 } 95 mTransparentPolicyState.reset(); 96 // In case mActivityRecord.hasCompatDisplayInsetsWithoutOverride() we don't apply the 97 // opaque activity constraints because we're expecting the activity is already letterboxed. 98 final ActivityRecord firstOpaqueActivity = mActivityRecord.getTask().getActivity( 99 FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE /* callback */, 100 mActivityRecord /* boundary */, false /* includeBoundary */, 101 true /* traverseTopToBottom */); 102 // We check if we need for some reason to skip the policy gievn the specific first 103 // opaque activity 104 if (shouldSkipTransparentPolicy(firstOpaqueActivity)) { 105 return; 106 } 107 mTransparentPolicyState.start(firstOpaqueActivity); 108 } 109 stop()110 void stop() { 111 for (int i = mDestroyListeners.size() - 1; i >= 0; i--) { 112 mDestroyListeners.get(i).start(); 113 } 114 mDestroyListeners.clear(); 115 mTransparentPolicyState.reset(); 116 } 117 118 /** 119 * @return {@code true} if the current activity is translucent with an opaque activity 120 * beneath and the related policy is running. In this case it will inherit bounds, orientation 121 * and aspect ratios from the first opaque activity beneath. 122 */ isRunning()123 boolean isRunning() { 124 return mTransparentPolicyState.isRunning(); 125 } 126 127 /** 128 * @return {@code true} if the current activity is translucent with an opaque activity 129 * beneath and needs to inherit its orientation. 130 */ hasInheritedOrientation()131 boolean hasInheritedOrientation() { 132 // To avoid wrong behaviour (e.g. permission dialogs not centered or with wrong size), 133 // transparent activities inherit orientation from the first opaque activity below only if 134 // they explicitly define an orientation different from SCREEN_ORIENTATION_UNSPECIFIED. 135 return isRunning() 136 && mActivityRecord.getOverrideOrientation() 137 != SCREEN_ORIENTATION_UNSPECIFIED; 138 } 139 getInheritedMinAspectRatio()140 float getInheritedMinAspectRatio() { 141 return mTransparentPolicyState.mInheritedMinAspectRatio; 142 } 143 getInheritedMaxAspectRatio()144 float getInheritedMaxAspectRatio() { 145 return mTransparentPolicyState.mInheritedMaxAspectRatio; 146 } 147 getInheritedAppCompatState()148 int getInheritedAppCompatState() { 149 return mTransparentPolicyState.mInheritedAppCompatState; 150 } 151 152 @Configuration.Orientation getInheritedOrientation()153 int getInheritedOrientation() { 154 return mTransparentPolicyState.mInheritedOrientation; 155 } 156 getInheritedCompatDisplayInsets()157 ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() { 158 return mTransparentPolicyState.mInheritedCompatDisplayInsets; 159 } 160 clearInheritedCompatDisplayInsets()161 void clearInheritedCompatDisplayInsets() { 162 mTransparentPolicyState.clearInheritedCompatDisplayInsets(); 163 } 164 getTransparentPolicyState()165 TransparentPolicyState getTransparentPolicyState() { 166 return mTransparentPolicyState; 167 } 168 169 /** 170 * In case of translucent activities, it consumes the {@link ActivityRecord} of the first opaque 171 * activity beneath using the given consumer and returns {@code true}. 172 */ applyOnOpaqueActivityBelow(@onNull Consumer<ActivityRecord> consumer)173 boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) { 174 return mTransparentPolicyState.applyOnOpaqueActivityBelow(consumer); 175 } 176 177 @NonNull getFirstOpaqueActivity()178 Optional<ActivityRecord> getFirstOpaqueActivity() { 179 return isRunning() ? Optional.of(mTransparentPolicyState.mFirstOpaqueActivity) 180 : Optional.empty(); 181 } 182 183 /** 184 * @return The first not finishing opaque activity beneath the current translucent activity 185 * if it exists and the strategy is enabled. 186 */ findOpaqueNotFinishingActivityBelow()187 Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() { 188 return mTransparentPolicyState.findOpaqueNotFinishingActivityBelow(); 189 } 190 191 // We evaluate the case when the policy should not be applied. shouldSkipTransparentPolicy(@ullable ActivityRecord opaqueActivity)192 private boolean shouldSkipTransparentPolicy(@Nullable ActivityRecord opaqueActivity) { 193 if (opaqueActivity == null || opaqueActivity.isEmbedded()) { 194 // We skip letterboxing if the translucent activity doesn't have any 195 // opaque activities beneath or the activity below is embedded which 196 // never has letterbox. 197 mActivityRecord.recomputeConfiguration(); 198 return true; 199 } 200 if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent() 201 || mActivityRecord.hasCompatDisplayInsetsWithoutInheritance()) { 202 return true; 203 } 204 return false; 205 } 206 207 /** Resets the screen size related fields so they can be resolved by requested bounds later. */ resetTranslucentOverrideConfig(Configuration config)208 private static void resetTranslucentOverrideConfig(Configuration config) { 209 // The values for the following properties will be defined during the configuration 210 // resolution in {@link ActivityRecord#resolveOverrideConfiguration} using the 211 // properties inherited from the first not finishing opaque activity beneath. 212 config.orientation = ORIENTATION_UNDEFINED; 213 config.screenWidthDp = config.compatScreenWidthDp = SCREEN_WIDTH_DP_UNDEFINED; 214 config.screenHeightDp = config.compatScreenHeightDp = SCREEN_HEIGHT_DP_UNDEFINED; 215 config.smallestScreenWidthDp = config.compatSmallestScreenWidthDp = 216 SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; 217 } 218 inheritConfiguration(ActivityRecord firstOpaque)219 private void inheritConfiguration(ActivityRecord firstOpaque) { 220 mTransparentPolicyState.inheritFromOpaque(firstOpaque); 221 } 222 223 /** 224 * Encapsulate the state for the current translucent activity when the transparent policy 225 * has started. 226 */ 227 static class TransparentPolicyState { 228 // Aspect ratio value to consider as undefined. 229 private static final float UNDEFINED_ASPECT_RATIO = 0f; 230 231 @NonNull 232 private final ActivityRecord mActivityRecord; 233 234 @Configuration.Orientation 235 private int mInheritedOrientation = ORIENTATION_UNDEFINED; 236 private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO; 237 private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO; 238 239 // The app compat state for the opaque activity if any 240 private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN; 241 242 // The CompatDisplayInsets of the opaque activity beneath the translucent one. 243 @Nullable 244 private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets; 245 246 @Nullable 247 private ActivityRecord mFirstOpaqueActivity; 248 249 /* 250 * WindowContainerListener responsible to make translucent activities inherit 251 * constraints from the first opaque activity beneath them. It's null for not 252 * translucent activities. 253 */ 254 @Nullable 255 private WindowContainerListener mLetterboxConfigListener; 256 TransparentPolicyState(@onNull ActivityRecord activityRecord)257 TransparentPolicyState(@NonNull ActivityRecord activityRecord) { 258 mActivityRecord = activityRecord; 259 } 260 start(@onNull ActivityRecord firstOpaqueActivity)261 private void start(@NonNull ActivityRecord firstOpaqueActivity) { 262 mFirstOpaqueActivity = firstOpaqueActivity; 263 mFirstOpaqueActivity.mTransparentPolicy 264 .mDestroyListeners.add(mActivityRecord.mTransparentPolicy); 265 inheritFromOpaque(firstOpaqueActivity); 266 final WindowContainer<?> parent = mActivityRecord.getParent(); 267 mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation( 268 mActivityRecord, mFirstOpaqueActivity, 269 (opaqueConfig, transparentOverrideConfig) -> { 270 resetTranslucentOverrideConfig(transparentOverrideConfig); 271 final Rect parentBounds = parent.getWindowConfiguration().getBounds(); 272 final Rect bounds = transparentOverrideConfig 273 .windowConfiguration.getBounds(); 274 final Rect letterboxBounds = opaqueConfig.windowConfiguration.getBounds(); 275 // We cannot use letterboxBounds directly here because the position relies 276 // on letterboxing. Using letterboxBounds directly, would produce a 277 // double offset. 278 bounds.set(parentBounds.left, parentBounds.top, 279 parentBounds.left + letterboxBounds.width(), 280 parentBounds.top + letterboxBounds.height()); 281 // We need to initialize appBounds to avoid NPE. The actual value will 282 // be set ahead when resolving the Configuration for the activity. 283 transparentOverrideConfig.windowConfiguration.setAppBounds(new Rect()); 284 inheritFromOpaque(mFirstOpaqueActivity); 285 return transparentOverrideConfig; 286 }); 287 } 288 inheritFromOpaque(@onNull ActivityRecord opaqueActivity)289 private void inheritFromOpaque(@NonNull ActivityRecord opaqueActivity) { 290 // To avoid wrong behaviour, we're not forcing a specific aspect ratio to activities 291 // which are not already providing one (e.g. permission dialogs) and presumably also 292 // not resizable. 293 if (mActivityRecord.getMinAspectRatio() != UNDEFINED_ASPECT_RATIO) { 294 mInheritedMinAspectRatio = opaqueActivity.getMinAspectRatio(); 295 } 296 if (mActivityRecord.getMaxAspectRatio() != UNDEFINED_ASPECT_RATIO) { 297 mInheritedMaxAspectRatio = opaqueActivity.getMaxAspectRatio(); 298 } 299 mInheritedOrientation = opaqueActivity.getRequestedConfigurationOrientation(); 300 mInheritedAppCompatState = opaqueActivity.getAppCompatState(); 301 mInheritedCompatDisplayInsets = opaqueActivity.getCompatDisplayInsets(); 302 } 303 reset()304 private void reset() { 305 if (mLetterboxConfigListener != null) { 306 mLetterboxConfigListener.onRemoved(); 307 } 308 mLetterboxConfigListener = null; 309 mInheritedOrientation = ORIENTATION_UNDEFINED; 310 mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO; 311 mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO; 312 mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN; 313 mInheritedCompatDisplayInsets = null; 314 if (mFirstOpaqueActivity != null) { 315 mFirstOpaqueActivity.mTransparentPolicy 316 .mDestroyListeners.remove(mActivityRecord.mTransparentPolicy); 317 } 318 mFirstOpaqueActivity = null; 319 } 320 isRunning()321 private boolean isRunning() { 322 return mLetterboxConfigListener != null; 323 } 324 clearInheritedCompatDisplayInsets()325 private void clearInheritedCompatDisplayInsets() { 326 mInheritedCompatDisplayInsets = null; 327 } 328 329 /** 330 * @return The first not finishing opaque activity beneath the current translucent activity 331 * if it exists and the strategy is enabled. 332 */ findOpaqueNotFinishingActivityBelow()333 private Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() { 334 if (!isRunning() || mActivityRecord.getTask() == null) { 335 return Optional.empty(); 336 } 337 return Optional.ofNullable(mFirstOpaqueActivity); 338 } 339 340 /** 341 * In case of translucent activities, it consumes the {@link ActivityRecord} of the first 342 * opaque activity beneath using the given consumer and returns {@code true}. 343 */ applyOnOpaqueActivityBelow(@onNull Consumer<ActivityRecord> consumer)344 private boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) { 345 return findOpaqueNotFinishingActivityBelow() 346 .map(activityRecord -> { 347 consumer.accept(activityRecord); 348 return true; 349 }).orElse(false); 350 } 351 } 352 353 } 354