• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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