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