WindowBoundsHelper.java
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.window;
import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.util.Log;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.View;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Helper class used to compute window bounds across Android versions. Obtain an instance with
* {@link #getInstance()}.
*/
class WindowBoundsHelper {
private static final String TAG = "WindowBoundsHelper";
private static WindowBoundsHelper sInstance = new WindowBoundsHelper();
@Nullable
private static WindowBoundsHelper sTestInstance;
static WindowBoundsHelper getInstance() {
if (sTestInstance != null) {
return sTestInstance;
}
return sInstance;
}
@VisibleForTesting
static void setForTesting(@Nullable WindowBoundsHelper helper) {
sTestInstance = helper;
}
WindowBoundsHelper() {}
/**
* Computes the size and position of the area the window would occupy with
* {@link android.view.WindowManager.LayoutParams#MATCH_PARENT MATCH_PARENT} width and height
* and any combination of flags that would allow the window to extend behind display cutouts.
* <p>
* For example, {@link android.view.WindowManager.LayoutParams#layoutInDisplayCutoutMode} set to
* {@link android.view.WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS} or the
* {@link android.view.WindowManager.LayoutParams#FLAG_LAYOUT_NO_LIMITS} flag set.
* <p>
* The value returned from this method may be different from platform API(s) used to determine
* the size and position of the visible area a given context occupies. For example:
* <ul>
* <li>{@link Display#getSize(Point)} can be used to determine the size of the visible area
* a window occupies, but may be subtracted to exclude certain system decorations that
* always appear on screen, notably the navigation bar.
* <li>The decor view's {@link View#getWidth()} and {@link View#getHeight()} can be used to
* determine the size of the top level view in the view hierarchy, but this size is
* determined through a combination of {@link android.view.WindowManager.LayoutParams}
* flags and may not represent the true window size. For example, a window that does not
* indicate it can be displayed behind a display cutout will have the size of the decor
* view offset to exclude this region unless this region overlaps with the status bar, while
* the value returned from this method will include this region.
* </ul>
* <p>
* The value returned from this method is guaranteed to be correct on platforms
* {@link Build.VERSION_CODES#Q Q} and above. For older platforms the value may be invalid if
* the activity is in multi-window mode or if the navigation bar offset can not be accounted
* for, though a best effort is made to ensure the returned value is as close as possible to
* the true value. See {@link #computeWindowBoundsP(Activity)} and
* {@link #computeWindowBoundsN(Activity)}.
* <p>
* Note: The value of this is based on the last windowing state reported to the client.
*
* @see android.view.WindowManager#getCurrentWindowMetrics()
* @see android.view.WindowMetrics#getBounds()
*/
@NonNull
Rect computeCurrentWindowBounds(Activity activity) {
if (Build.VERSION.SDK_INT >= R) {
return activity.getWindowManager().getCurrentWindowMetrics().getBounds();
} else if (Build.VERSION.SDK_INT >= Q) {
return computeWindowBoundsQ(activity);
} else if (Build.VERSION.SDK_INT >= P) {
return computeWindowBoundsP(activity);
} else if (Build.VERSION.SDK_INT >= N) {
return computeWindowBoundsN(activity);
} else {
return computeWindowBoundsIceCreamSandwich(activity);
}
}
/**
* Computes the maximum size and position of the area the window can expect with
* {@link android.view.WindowManager.LayoutParams#MATCH_PARENT MATCH_PARENT} width and height
* and any combination of flags that would allow the window to extend behind display cutouts.
* <p>
* The value returned from this method will always match {@link Display#getRealSize(Point)} on
* {@link Build.VERSION_CODES#Q Android 10} and below.
*
* @see android.view.WindowManager#getMaximumWindowMetrics()
*/
@NonNull
Rect computeMaximumWindowBounds(Activity activity) {
if (Build.VERSION.SDK_INT >= R) {
return activity.getWindowManager().getMaximumWindowMetrics().getBounds();
} else {
Display display = activity.getWindowManager().getDefaultDisplay();
Point displaySize = getRealSizeForDisplay(display);
return new Rect(0, 0, displaySize.x, displaySize.y);
}
}
/** Computes the window bounds for {@link Build.VERSION_CODES#Q}. */
@NonNull
@RequiresApi(Q)
private static Rect computeWindowBoundsQ(Activity activity) {
Rect bounds;
Configuration config = activity.getResources().getConfiguration();
try {
Field windowConfigField = Configuration.class.getDeclaredField("windowConfiguration");
windowConfigField.setAccessible(true);
Object windowConfig = windowConfigField.get(config);
Method getBoundsMethod = windowConfig.getClass().getDeclaredMethod("getBounds");
bounds = new Rect((Rect) getBoundsMethod.invoke(windowConfig));
} catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException
| InvocationTargetException e) {
Log.w(TAG, e);
// If reflection fails for some reason default to the P implementation which still has
// the ability to account for display cutouts.
bounds = computeWindowBoundsP(activity);
}
return bounds;
}
/**
* Computes the window bounds for {@link Build.VERSION_CODES#P}.
* <p>
* NOTE: This method may result in incorrect values if the {@link Resources} value stored at
* 'navigation_bar_height' does not match the true navigation bar inset on the window.
* </ul>
*/
@NonNull
@RequiresApi(P)
private static Rect computeWindowBoundsP(Activity activity) {
Rect bounds = new Rect();
Configuration config = activity.getResources().getConfiguration();
try {
Field windowConfigField = Configuration.class.getDeclaredField("windowConfiguration");
windowConfigField.setAccessible(true);
Object windowConfig = windowConfigField.get(config);
// In multi-window mode we'll use the WindowConfiguration#mBounds property which
// should match the window size. Otherwise we'll use the mAppBounds property and will
// adjust it below.
if (activity.isInMultiWindowMode()) {
Method getAppBounds = windowConfig.getClass().getDeclaredMethod("getBounds");
bounds.set((Rect) getAppBounds.invoke(windowConfig));
} else {
Method getAppBounds = windowConfig.getClass().getDeclaredMethod("getAppBounds");
bounds.set((Rect) getAppBounds.invoke(windowConfig));
}
} catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException
| InvocationTargetException e) {
Log.w(TAG, e);
Display defaultDisplay = activity.getWindowManager().getDefaultDisplay();
defaultDisplay.getRectSize(bounds);
}
android.view.WindowManager platformWindowManager = activity.getWindowManager();
Display currentDisplay = platformWindowManager.getDefaultDisplay();
Point realDisplaySize = new Point();
currentDisplay.getRealSize(realDisplaySize);
if (!activity.isInMultiWindowMode()) {
// The activity is not in multi-window mode. Check if the addition of the navigation
// bar size to mAppBounds results in the real display size and if so assume the nav
// bar height should be added to the result.
int navigationBarHeight = getNavigationBarHeight(activity);
if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
bounds.bottom += navigationBarHeight;
} else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
bounds.right += navigationBarHeight;
} else if (bounds.left == navigationBarHeight) {
bounds.left = 0;
}
}
if ((bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y)
&& !activity.isInMultiWindowMode()) {
// If the corrected bounds are not the same as the display size and the activity is not
// in multi-window mode it is possible there are unreported cutouts inset-ing the
// window depending on the layoutInCutoutMode. Check for them here by getting the
// cutout from the display itself.
DisplayCutout displayCutout = getCutoutForDisplay(currentDisplay);
if (displayCutout != null) {
if (bounds.left == displayCutout.getSafeInsetLeft()) {
bounds.left = 0;
}
if (realDisplaySize.x - bounds.right == displayCutout.getSafeInsetRight()) {
bounds.right += displayCutout.getSafeInsetRight();
}
if (bounds.top == displayCutout.getSafeInsetTop()) {
bounds.top = 0;
}
if (realDisplaySize.y - bounds.bottom == displayCutout.getSafeInsetBottom()) {
bounds.bottom += displayCutout.getSafeInsetBottom();
}
}
}
return bounds;
}
/**
* Computes the window bounds for platforms between {@link Build.VERSION_CODES#N}
* and {@link Build.VERSION_CODES#O_MR1}, inclusive.
* <p>
* NOTE: This method may result in incorrect values under the following conditions:
* <ul>
* <li>If the activity is in multi-window mode the origin of the returned bounds will
* always be anchored at (0, 0).
* <li>If the {@link Resources} value stored at 'navigation_bar_height' does not match the
* true navigation bar size the returned bounds will not take into account the navigation
* bar.
* </ul>
*/
@NonNull
@RequiresApi(N)
private static Rect computeWindowBoundsN(Activity activity) {
Rect bounds = new Rect();
Display defaultDisplay = activity.getWindowManager().getDefaultDisplay();
defaultDisplay.getRectSize(bounds);
if (!activity.isInMultiWindowMode()) {
// The activity is not in multi-window mode. Check if the addition of the navigation
// bar size to Display#getSize() results in the real display size and if so return
// this value. If not, return the result of Display#getSize().
Point realDisplaySize = getRealSizeForDisplay(defaultDisplay);
int navigationBarHeight = getNavigationBarHeight(activity);
if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
bounds.bottom += navigationBarHeight;
} else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
bounds.right += navigationBarHeight;
}
}
return bounds;
}
/**
* Computes the window bounds for platforms between {@link Build.VERSION_CODES#JELLY_BEAN}
* and {@link Build.VERSION_CODES#M}, inclusive.
* <p>
* Given that multi-window mode isn't supported before N we simply return the real display
* size which should match the window size of a full-screen app.
*/
@NonNull
@RequiresApi(ICE_CREAM_SANDWICH)
private static Rect computeWindowBoundsIceCreamSandwich(Activity activity) {
Display defaultDisplay = activity.getWindowManager().getDefaultDisplay();
Point realDisplaySize = getRealSizeForDisplay(defaultDisplay);
Rect bounds = new Rect();
if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {
defaultDisplay.getRectSize(bounds);
} else {
bounds.right = realDisplaySize.x;
bounds.bottom = realDisplaySize.y;
}
return bounds;
}
/**
* Returns the full (real) size of the display, in pixels, without subtracting any window
* decor or applying any compatibility scale factors.
* <p>
* The size is adjusted based on the current rotation of the display.
*
* @return a point representing the real display size in pixels.
*
* @see Display#getRealSize(Point)
*/
@NonNull
@VisibleForTesting
@RequiresApi(ICE_CREAM_SANDWICH)
static Point getRealSizeForDisplay(Display display) {
Point size = new Point();
if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR1) {
display.getRealSize(size);
} else {
try {
Method getRealSizeMethod = Display.class.getDeclaredMethod("getRealSize",
Point.class);
getRealSizeMethod.setAccessible(true);
getRealSizeMethod.invoke(display, size);
} catch (NoSuchMethodException e) {
Log.w(TAG, e);
} catch (IllegalAccessException e) {
Log.w(TAG, e);
} catch (InvocationTargetException e) {
Log.w(TAG, e);
}
}
return size;
}
/**
* Returns the {@link Resources} value stored as 'navigation_bar_height'.
* <p>
* Note: This is error-prone and is <b>not</b> the recommended way to determine the size
* of the overlapping region between the navigation bar and a given window. The best approach
* is to acquire the {@link WindowInsets}.
*/
private static int getNavigationBarHeight(Context context) {
Resources resources = context.getResources();
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
return resources.getDimensionPixelSize(resourceId);
}
return 0;
}
/**
* Returns the {@link DisplayCutout} for the given display. Note that display cutout returned
* here is for the display and the insets provided are in the display coordinate system.
*
* @return the display cutout for the given display.
*/
@Nullable
@RequiresApi(P)
private static DisplayCutout getCutoutForDisplay(Display display) {
DisplayCutout displayCutout = null;
try {
Class<?> displayInfoClass = Class.forName("android.view.DisplayInfo");
Constructor<?> displayInfoConstructor = displayInfoClass.getConstructor();
displayInfoConstructor.setAccessible(true);
Object displayInfo = displayInfoConstructor.newInstance();
Method getDisplayInfoMethod = display.getClass().getDeclaredMethod(
"getDisplayInfo", displayInfo.getClass());
getDisplayInfoMethod.setAccessible(true);
getDisplayInfoMethod.invoke(display, displayInfo);
Field displayCutoutField = displayInfo.getClass().getDeclaredField("displayCutout");
displayCutoutField.setAccessible(true);
Object cutout = displayCutoutField.get(displayInfo);
if (cutout instanceof DisplayCutout) {
displayCutout = (DisplayCutout) cutout;
}
} catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException
| IllegalAccessException | InvocationTargetException
| InstantiationException e) {
Log.w(TAG, e);
}
return displayCutout;
}
}