chromium/components/browser_ui/util/android/java/src/org/chromium/components/browser_ui/util/DimensionCompat.java

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.util;

import android.app.Activity;
import android.graphics.Point;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.WindowInsets;

import androidx.annotation.Px;
import androidx.annotation.RequiresApi;

/** Collection of methods computing various height dimensions that differ by OS build version. */
public abstract class DimensionCompat {
    protected final Activity mActivity;
    protected final Runnable mPositionUpdater;

    /**
     * @param activity {@link Activity} in which the UI dimensions are queried
     * @param positionUpdater {@link Runnable} to be invoked to reflect the app content frame
     *        size updates if it resizes dynamically in the course of app lifecycle.
     */
    public static DimensionCompat create(Activity activity, Runnable positionUpdater) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            return new DimensionCompatLegacy(activity, positionUpdater);
        }
        return new DimensionCompatR(activity, positionUpdater);
    }

    /** Updates the tab's height/size after user interaction/device rotation */
    public abstract void updatePosition();

    /**
     * Returns window height including all system bar areas. In multi-window mode, returns the
     * window height of the current app excluding the navigation bar. In non-MW mode, it includes
     * the navigation bar height as well.
     */
    public abstract @Px int getWindowHeight();

    /** Returns window width */
    public abstract @Px int getWindowWidth();

    /** Returns the status bar height */
    public abstract @Px int getStatusbarHeight();

    /** Returns the bottom navigation bar height */
    public abstract @Px int getNavbarHeight();

    /** Implementation that supports R+ */
    @RequiresApi(Build.VERSION_CODES.R)
    private static class DimensionCompatR extends DimensionCompat {
        private DimensionCompatR(Activity activity, Runnable positionUpdater) {
            super(activity, positionUpdater);
        }

        @Override
        public void updatePosition() {
            mPositionUpdater.run();
        }

        @Override
        @Px
        public int getWindowHeight() {
            return mActivity.getWindowManager().getCurrentWindowMetrics().getBounds().height();
        }

        @Override
        @Px
        public int getWindowWidth() {
            return mActivity.getWindowManager().getCurrentWindowMetrics().getBounds().width();
        }

        @Override
        @Px
        public int getStatusbarHeight() {
            return mActivity
                    .getWindowManager()
                    .getCurrentWindowMetrics()
                    .getWindowInsets()
                    .getInsets(WindowInsets.Type.statusBars())
                    .top;
        }

        @Override
        @Px
        public int getNavbarHeight() {
            return mActivity
                    .getWindowManager()
                    .getCurrentWindowMetrics()
                    .getWindowInsets()
                    .getInsets(WindowInsets.Type.navigationBars())
                    .bottom;
        }
    }

    DimensionCompat(Activity activity, Runnable positionUpdater) {
        mActivity = activity;
        mPositionUpdater = positionUpdater;
    }

    /** Implementation that supports version below R */
    private static class DimensionCompatLegacy extends DimensionCompat {
        private DimensionCompatLegacy(Activity activity, Runnable positionUpdater) {
            super(activity, positionUpdater);
        }

        @Override
        public void updatePosition() {
            // On pre-R devices, We wait till the layout is complete and get the content
            // |android.R.id.content| view height. See |getAppUsableScreenHeightFromContent|.
            View contentFrame = mActivity.findViewById(android.R.id.content);
            // Maybe invoked before layout inflation? Simply return here - position update will be
            // attempted later again by |onPostInflationStartUp|.
            if (contentFrame == null) return;

            contentFrame.addOnLayoutChangeListener(
                    new View.OnLayoutChangeListener() {
                        @Override
                        public void onLayoutChange(
                                View v,
                                int left,
                                int top,
                                int right,
                                int bottom,
                                int oldLeft,
                                int oldTop,
                                int oldRight,
                                int oldBottom) {
                            contentFrame.removeOnLayoutChangeListener(this);
                            mPositionUpdater.run();
                        }
                    });
        }

        @Override
        @Px
        public int getWindowHeight() {
            return getDisplayMetrics().heightPixels;
        }

        @Override
        @Px
        public int getWindowWidth() {
            return getDisplayMetrics().widthPixels;
        }

        @Override
        @SuppressWarnings({"DiscouragedApi", "InternalInsetResource"})
        @Px
        public int getStatusbarHeight() {
            int statusBarHeight = 0;
            final int statusBarHeightResourceId =
                    mActivity.getResources().getIdentifier("status_bar_height", "dimen", "android");
            if (statusBarHeightResourceId > 0) {
                statusBarHeight =
                        mActivity.getResources().getDimensionPixelSize(statusBarHeightResourceId);
            }
            return statusBarHeight;
        }

        @Override
        @Px
        public int getNavbarHeight() {
            // Pre-R OS offers no official way to get the navigation bar height. A common way was
            // to get it from a resource definition('navigation_bar_height') but it fails on some
            // vendor-customized devices.
            // A workaround here is to subtract the app-usable height from the whole display height.
            // There are a couple of ways to get the app-usable height:
            // 1) content frame + status bar height
            // 2) |display.getSize|
            // On some devices, only one returns the right height, the other returning a height
            // bigger that the actual value. Heuristically we choose the smaller of the two.
            return getWindowHeight()
                    - Math.max(
                            getAppUsableScreenHeightFromContent(),
                            getAppUsableScreenHeightFromDisplay());
        }

        // TODO(jinsukkim): Explore the way to use androidx.window.WindowManager or
        // androidx.window.java.WindowInfoRepoJavaAdapter once the androidx API get finalized and is
        // available in Chromium to use #getCurrentWindowMetrics()/#currentWindowMetrics() to get
        // the height of the display our Window currently in.
        //
        // The #getRealMetrics() method will give the physical size of the screen, which is
        // generally fine when the app is not in multi-window mode and #getMetrics() will give the
        // height excludes the decor views, so not suitable for our case. But in multi-window mode,
        // we have no much choice, the closest way is to use #getMetrics() method, because we need
        // to handle rotation.
        private DisplayMetrics getDisplayMetrics() {
            DisplayMetrics displayMetrics = new DisplayMetrics();
            if (mActivity.isInMultiWindowMode()) {
                mActivity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
            } else {
                mActivity.getWindowManager().getDefaultDisplay().getRealMetrics(displayMetrics);
            }
            return displayMetrics;
        }

        private int getAppUsableScreenHeightFromContent() {
            // A correct way to get the client area height would be to use the parent of |content|
            // to make sure to include the top action bar dimension. But CCT (or Chrome for that
            // matter) doesn't have the top action bar. So getting the height of |content| is
            // enough.
            View contentFrame = mActivity.findViewById(android.R.id.content);
            return contentFrame.getHeight() + getStatusbarHeight();
        }

        private int getAppUsableScreenHeightFromDisplay() {
            Display display = mActivity.getWindowManager().getDefaultDisplay();
            Point size = new Point();
            display.getSize(size);
            return size.y;
        }
    }
}