• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.launcher3.util.window;
17 
18 import static android.view.Display.DEFAULT_DISPLAY;
19 
20 import static com.android.launcher3.Utilities.dpiFromPx;
21 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
22 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT;
23 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT_LANDSCAPE;
24 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE;
25 import static com.android.launcher3.testing.shared.ResourceUtils.NAV_BAR_INTERACTION_MODE_RES_NAME;
26 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT;
27 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT_LANDSCAPE;
28 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT_PORTRAIT;
29 import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
30 import static com.android.launcher3.util.RotationUtils.deltaRotation;
31 import static com.android.launcher3.util.RotationUtils.rotateRect;
32 import static com.android.launcher3.util.RotationUtils.rotateSize;
33 
34 import android.annotation.TargetApi;
35 import android.content.Context;
36 import android.content.res.Configuration;
37 import android.content.res.Resources;
38 import android.graphics.Insets;
39 import android.graphics.Point;
40 import android.graphics.Rect;
41 import android.hardware.display.DisplayManager;
42 import android.os.Build;
43 import android.util.ArrayMap;
44 import android.util.Log;
45 import android.view.Display;
46 import android.view.DisplayCutout;
47 import android.view.Surface;
48 import android.view.WindowInsets;
49 import android.view.WindowManager;
50 import android.view.WindowMetrics;
51 
52 import com.android.launcher3.R;
53 import com.android.launcher3.Utilities;
54 import com.android.launcher3.testing.shared.ResourceUtils;
55 import com.android.launcher3.util.MainThreadInitializedObject;
56 import com.android.launcher3.util.NavigationMode;
57 import com.android.launcher3.util.ResourceBasedOverride;
58 import com.android.launcher3.util.WindowBounds;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 /**
64  * Utility class for mocking some window manager behaviours
65  */
66 public class WindowManagerProxy implements ResourceBasedOverride {
67 
68     private static final String TAG = "WindowManagerProxy";
69     public static final int MIN_TABLET_WIDTH = 600;
70 
71     public static final MainThreadInitializedObject<WindowManagerProxy> INSTANCE =
72             forOverride(WindowManagerProxy.class, R.string.window_manager_proxy_class);
73 
74     protected final boolean mTaskbarDrawnInProcess;
75 
76     /**
77      * Creates a new instance of proxy, applying any overrides
78      */
newInstance(Context context)79     public static WindowManagerProxy newInstance(Context context) {
80         return Overrides.getObject(WindowManagerProxy.class, context,
81                 R.string.window_manager_proxy_class);
82     }
83 
WindowManagerProxy()84     public WindowManagerProxy() {
85         this(false);
86     }
87 
WindowManagerProxy(boolean taskbarDrawnInProcess)88     protected WindowManagerProxy(boolean taskbarDrawnInProcess) {
89         mTaskbarDrawnInProcess = taskbarDrawnInProcess;
90     }
91 
92     /**
93      * Returns a map of normalized info of internal displays to estimated window bounds
94      * for that display
95      */
estimateInternalDisplayBounds( Context displayInfoContext)96     public ArrayMap<CachedDisplayInfo, List<WindowBounds>> estimateInternalDisplayBounds(
97             Context displayInfoContext) {
98         CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize();
99         List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info);
100         ArrayMap<CachedDisplayInfo, List<WindowBounds>> result = new ArrayMap<>();
101         result.put(info, bounds);
102         return result;
103     }
104 
105     /**
106      * Returns the real bounds for the provided display after applying any insets normalization
107      */
108     @TargetApi(Build.VERSION_CODES.R)
getRealBounds(Context displayInfoContext, CachedDisplayInfo info)109     public WindowBounds getRealBounds(Context displayInfoContext, CachedDisplayInfo info) {
110         if (!Utilities.ATLEAST_R) {
111             Point smallestSize = new Point();
112             Point largestSize = new Point();
113             getDisplay(displayInfoContext).getCurrentSizeRange(smallestSize, largestSize);
114 
115             if (info.size.y > info.size.x) {
116                 // Portrait
117                 return new WindowBounds(info.size.x, info.size.y, smallestSize.x, largestSize.y,
118                         info.rotation);
119             } else {
120                 // Landscape
121                 return new WindowBounds(info.size.x, info.size.y, largestSize.x, smallestSize.y,
122                         info.rotation);
123             }
124         }
125 
126         WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class)
127                 .getMaximumWindowMetrics();
128         Rect insets = new Rect();
129         normalizeWindowInsets(displayInfoContext, windowMetrics.getWindowInsets(), insets);
130         return new WindowBounds(windowMetrics.getBounds(), insets, info.rotation);
131     }
132 
133     /**
134      * Returns an updated insets, accounting for various Launcher UI specific overrides like taskbar
135      */
136     @TargetApi(Build.VERSION_CODES.R)
normalizeWindowInsets(Context context, WindowInsets oldInsets, Rect outInsets)137     public WindowInsets normalizeWindowInsets(Context context, WindowInsets oldInsets,
138             Rect outInsets) {
139         if (!Utilities.ATLEAST_R || !mTaskbarDrawnInProcess) {
140             outInsets.set(oldInsets.getSystemWindowInsetLeft(), oldInsets.getSystemWindowInsetTop(),
141                     oldInsets.getSystemWindowInsetRight(), oldInsets.getSystemWindowInsetBottom());
142             return oldInsets;
143         }
144 
145         WindowInsets.Builder insetsBuilder = new WindowInsets.Builder(oldInsets);
146         Insets navInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars());
147 
148         Resources systemRes = context.getResources();
149         Configuration config = systemRes.getConfiguration();
150 
151         boolean isTablet = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
152         boolean isGesture = isGestureNav(context);
153         boolean isPortrait = config.screenHeightDp > config.screenWidthDp;
154 
155         int bottomNav = isTablet
156                 ? 0
157                 : (isPortrait
158                         ? getDimenByName(systemRes, NAVBAR_HEIGHT)
159                         : (isGesture
160                                 ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE)
161                                 : 0));
162         Insets newNavInsets = Insets.of(navInsets.left, navInsets.top, navInsets.right, bottomNav);
163         insetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets);
164         insetsBuilder.setInsetsIgnoringVisibility(WindowInsets.Type.navigationBars(), newNavInsets);
165 
166         Insets statusBarInsets = oldInsets.getInsets(WindowInsets.Type.statusBars());
167 
168         Insets newStatusBarInsets = Insets.of(
169                 statusBarInsets.left,
170                 getStatusBarHeight(context, isPortrait, statusBarInsets.top),
171                 statusBarInsets.right,
172                 statusBarInsets.bottom);
173         insetsBuilder.setInsets(WindowInsets.Type.statusBars(), newStatusBarInsets);
174         insetsBuilder.setInsetsIgnoringVisibility(
175                 WindowInsets.Type.statusBars(), newStatusBarInsets);
176 
177         // Override the tappable insets to be 0 on the bottom for gesture nav (otherwise taskbar
178         // would count towards it). This is used for the bottom protection in All Apps for example.
179         if (isGesture) {
180             Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement());
181             Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top,
182                     oldTappableInsets.right, 0);
183             insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets);
184         }
185 
186         WindowInsets result = insetsBuilder.build();
187         Insets systemWindowInsets = result.getInsetsIgnoringVisibility(
188                 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
189         outInsets.set(systemWindowInsets.left, systemWindowInsets.top, systemWindowInsets.right,
190                 systemWindowInsets.bottom);
191         return result;
192     }
193 
getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset)194     protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) {
195         Resources systemRes = context.getResources();
196         int statusBarHeight = getDimenByName(systemRes,
197                 isPortrait ? STATUS_BAR_HEIGHT_PORTRAIT : STATUS_BAR_HEIGHT_LANDSCAPE,
198                 STATUS_BAR_HEIGHT);
199 
200         return Math.max(statusBarInset, statusBarHeight);
201     }
202 
203     /**
204      * Returns a list of possible WindowBounds for the display keyed on the 4 surface rotations
205      */
estimateWindowBounds(Context context, CachedDisplayInfo displayInfo)206     protected List<WindowBounds> estimateWindowBounds(Context context,
207             CachedDisplayInfo displayInfo) {
208         int densityDpi = context.getResources().getConfiguration().densityDpi;
209         int rotation = displayInfo.rotation;
210         Rect safeCutout = displayInfo.cutout;
211 
212         int minSize = Math.min(displayInfo.size.x, displayInfo.size.y);
213         int swDp = (int) dpiFromPx(minSize, densityDpi);
214 
215         Resources systemRes;
216         {
217             Configuration conf = new Configuration();
218             conf.smallestScreenWidthDp = swDp;
219             systemRes = context.createConfigurationContext(conf).getResources();
220         }
221 
222         boolean isTablet = swDp >= MIN_TABLET_WIDTH;
223         boolean isTabletOrGesture = isTablet
224                 || (Utilities.ATLEAST_R && isGestureNav(context));
225 
226         // Use the status bar height resources because current system API to get the status bar
227         // height doesn't allow to do this for an arbitrary display, it returns value only
228         // for the current active display (see com.android.internal.policy.StatusBarUtils)
229         int statusBarHeightPortrait = getDimenByName(systemRes,
230                 STATUS_BAR_HEIGHT_PORTRAIT, STATUS_BAR_HEIGHT);
231         int statusBarHeightLandscape = getDimenByName(systemRes,
232                 STATUS_BAR_HEIGHT_LANDSCAPE, STATUS_BAR_HEIGHT);
233 
234         int navBarHeightPortrait, navBarHeightLandscape, navbarWidthLandscape;
235 
236         navBarHeightPortrait = isTablet
237                 ? (mTaskbarDrawnInProcess
238                         ? 0 : systemRes.getDimensionPixelSize(R.dimen.taskbar_size))
239                 : getDimenByName(systemRes, NAVBAR_HEIGHT);
240 
241         navBarHeightLandscape = isTablet
242                 ? (mTaskbarDrawnInProcess
243                         ? 0 : systemRes.getDimensionPixelSize(R.dimen.taskbar_size))
244                 : (isTabletOrGesture
245                         ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) : 0);
246         navbarWidthLandscape = isTabletOrGesture
247                 ? 0
248                 : getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
249 
250         List<WindowBounds> result = new ArrayList<>(4);
251         Point tempSize = new Point();
252         for (int i = 0; i < 4; i++) {
253             int rotationChange = deltaRotation(rotation, i);
254             tempSize.set(displayInfo.size.x, displayInfo.size.y);
255             rotateSize(tempSize, rotationChange);
256             Rect bounds = new Rect(0, 0, tempSize.x, tempSize.y);
257 
258             int navBarHeight, navbarWidth, statusBarHeight;
259             if (tempSize.y > tempSize.x) {
260                 navBarHeight = navBarHeightPortrait;
261                 navbarWidth = 0;
262                 statusBarHeight = statusBarHeightPortrait;
263             } else {
264                 navBarHeight = navBarHeightLandscape;
265                 navbarWidth = navbarWidthLandscape;
266                 statusBarHeight = statusBarHeightLandscape;
267             }
268 
269             Rect insets = new Rect(safeCutout);
270             rotateRect(insets, rotationChange);
271             insets.top = Math.max(insets.top, statusBarHeight);
272             insets.bottom = Math.max(insets.bottom, navBarHeight);
273 
274             if (i == Surface.ROTATION_270 || i == Surface.ROTATION_180) {
275                 // On reverse landscape (and in rare-case when the natural orientation of the
276                 // device is landscape), navigation bar is on the right.
277                 insets.left = Math.max(insets.left, navbarWidth);
278             } else {
279                 insets.right = Math.max(insets.right, navbarWidth);
280             }
281             result.add(new WindowBounds(bounds, insets, i));
282         }
283         return result;
284     }
285 
286     /**
287      * Wrapper around the utility method for easier emulation
288      */
getDimenByName(Resources res, String resName)289     protected int getDimenByName(Resources res, String resName) {
290         return ResourceUtils.getDimenByName(resName, res, 0);
291     }
292 
293     /**
294      * Wrapper around the utility method for easier emulation
295      */
getDimenByName(Resources res, String resName, String fallback)296     protected int getDimenByName(Resources res, String resName, String fallback) {
297         int dimen = ResourceUtils.getDimenByName(resName, res, -1);
298         return dimen > -1 ? dimen : getDimenByName(res, fallback);
299     }
300 
isGestureNav(Context context)301     protected boolean isGestureNav(Context context) {
302         return ResourceUtils.getIntegerByName("config_navBarInteractionMode",
303                 context.getResources(), INVALID_RESOURCE_HANDLE) == 2;
304     }
305 
306     /**
307      * Returns a CachedDisplayInfo initialized for the current display
308      */
309     @TargetApi(Build.VERSION_CODES.S)
getDisplayInfo(Context displayInfoContext)310     public CachedDisplayInfo getDisplayInfo(Context displayInfoContext) {
311         int rotation = getRotation(displayInfoContext);
312         if (Utilities.ATLEAST_S) {
313             WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class)
314                     .getMaximumWindowMetrics();
315             return getDisplayInfo(windowMetrics, rotation);
316         } else {
317             Point size = new Point();
318             Display display = getDisplay(displayInfoContext);
319             display.getRealSize(size);
320             Rect cutoutRect = new Rect();
321             return new CachedDisplayInfo(size, rotation, cutoutRect);
322         }
323     }
324 
325     /**
326      * Returns a CachedDisplayInfo initialized for the current display
327      */
328     @TargetApi(Build.VERSION_CODES.S)
getDisplayInfo(WindowMetrics windowMetrics, int rotation)329     protected CachedDisplayInfo getDisplayInfo(WindowMetrics windowMetrics, int rotation) {
330         Point size = new Point(windowMetrics.getBounds().right, windowMetrics.getBounds().bottom);
331         Rect cutoutRect = new Rect();
332         DisplayCutout cutout = windowMetrics.getWindowInsets().getDisplayCutout();
333         if (cutout != null) {
334             cutoutRect.set(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
335                     cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
336         }
337         return new CachedDisplayInfo(size, rotation, cutoutRect);
338     }
339 
340     /**
341      * Returns rotation of the display associated with the context, or rotation of DEFAULT_DISPLAY
342      * if the context isn't associated with a display.
343      */
getRotation(Context displayInfoContext)344     public int getRotation(Context displayInfoContext) {
345         return getDisplay(displayInfoContext).getRotation();
346     }
347 
348     /**
349      *
350      * Returns the display associated with the context, or DEFAULT_DISPLAY if the context isn't
351      * associated with a display.
352      */
getDisplay(Context displayInfoContext)353     protected Display getDisplay(Context displayInfoContext) {
354         if (Utilities.ATLEAST_R) {
355             try {
356                 return displayInfoContext.getDisplay();
357             } catch (UnsupportedOperationException e) {
358                 // Ignore
359             }
360         }
361         return displayInfoContext.getSystemService(DisplayManager.class).getDisplay(
362                 DEFAULT_DISPLAY);
363     }
364 
365     /**
366      * Returns the current navigation mode from resource.
367      */
getNavigationMode(Context context)368     public NavigationMode getNavigationMode(Context context) {
369         int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME,
370                 context.getResources(), INVALID_RESOURCE_HANDLE);
371 
372         if (modeInt == INVALID_RESOURCE_HANDLE) {
373             Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
374         } else {
375             for (NavigationMode m : NavigationMode.values()) {
376                 if (m.resValue == modeInt) {
377                     return m;
378                 }
379             }
380         }
381         return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON :
382                 NavigationMode.THREE_BUTTONS;
383     }
384 }
385