Add app bounds to configuration.

The system previously overrode the display size for a specific scope
(task/activity/etc.) by setting the associated Configuration's
screenWidthDp/screenHeightDp. This leads to two issues. First, the
conversion of screen size from pixels to display independent pixels
and then upconverting later on leads to rounding errors. Secondly,
the screenWidthDp and screenHeightDp values account for insets, such
as the status bar. These however are not reflected in the display
size when returned from Display#getMetrics/getSize.

This changelist addresses the issue by adding a Rect value to
Configuration which stores the app display bounds. This is always set
at the display level and overridden as appropriate. As the proper
app insets are accounted for at the root configuration, all overrides
(outside of specific exceptions) are the result of the intersection
between the requested bound and the parent bound.

Change-Id: I2c4fcd0bee92af12aabbca258de05b4ec061d0e1
Fixes: 34338931
Bug: 36812336
Bug: 36676979
Test: cts/hostsidetests/services/activityandwindowmanager/util/run-test CtsAppTestCases android.app.cts.AspectRatioTests
Test: cts/hostsidetests/services/activityandwindowmanager/util/run-test CtsServicesHostTestCases android.server.cts.ActivityManagerDisplayTests
Test: bit FrameworksServicesTests:com.android.server.wm.AppBoundsTests
(cherry picked from commit 7566d76c618a48b8dcc981dac3cab1e42f864063)
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index 99fbee1..c8353c9 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -16,6 +16,11 @@
 
 package android.content.res;
 
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.DisplayInfo;
 import com.android.internal.util.XmlUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -293,6 +298,16 @@
      */
     public int screenLayout;
 
+    /**
+     * @hide
+     * {@link android.graphics.Rect} defining app bounds. The dimensions override usages of
+     * {@link DisplayInfo#appHeight} and {@link DisplayInfo#appWidth} and mirrors these values at
+     * the display level. Lower levels can override these values to provide custom bounds to enforce
+     * features such as a max aspect ratio.
+     * TODO(b/36812336): Move appBounds out of {@link Configuration}.
+     */
+    public Rect appBounds;
+
     /** @hide */
     static public int resetScreenLayout(int curLayout) {
         return (curLayout&~(SCREENLAYOUT_LONG_MASK | SCREENLAYOUT_SIZE_MASK
@@ -882,6 +897,7 @@
         compatScreenWidthDp = o.compatScreenWidthDp;
         compatScreenHeightDp = o.compatScreenHeightDp;
         compatSmallestScreenWidthDp = o.compatSmallestScreenWidthDp;
+        setAppBounds(o.appBounds);
         assetsSeq = o.assetsSeq;
         seq = o.seq;
     }
@@ -1032,6 +1048,9 @@
             case NAVIGATIONHIDDEN_YES: sb.append("/h"); break;
             default: sb.append("/"); sb.append(navigationHidden); break;
         }
+        if (appBounds != null) {
+            sb.append(" appBounds="); sb.append(appBounds);
+        }
         if (assetsSeq != 0) {
             sb.append(" as.").append(assetsSeq);
         }
@@ -1066,6 +1085,7 @@
         smallestScreenWidthDp = compatSmallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
         densityDpi = DENSITY_DPI_UNDEFINED;
         assetsSeq = ASSETS_SEQ_UNDEFINED;
+        appBounds = null;
         seq = 0;
     }
 
@@ -1253,6 +1273,10 @@
         if (delta.compatSmallestScreenWidthDp != SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
             compatSmallestScreenWidthDp = delta.compatSmallestScreenWidthDp;
         }
+        if (delta.appBounds != null && !delta.appBounds.equals(appBounds)) {
+            changed |= ActivityInfo.CONFIG_SCREEN_SIZE;
+            setAppBounds(delta.appBounds);
+        }
         if (delta.assetsSeq != ASSETS_SEQ_UNDEFINED) {
             changed |= ActivityInfo.CONFIG_ASSETS_PATHS;
             assetsSeq = delta.assetsSeq;
@@ -1399,6 +1423,13 @@
             changed |= ActivityInfo.CONFIG_ASSETS_PATHS;
         }
 
+        // Make sure that one of the values is not null and that they are not equal.
+        if ((compareUndefined || delta.appBounds != null)
+                && appBounds != delta.appBounds
+                && (appBounds == null || !appBounds.equals(delta.appBounds))) {
+            changed |= ActivityInfo.CONFIG_SCREEN_SIZE;
+        }
+
         return changed;
     }
 
@@ -1494,6 +1525,7 @@
         dest.writeInt(compatScreenWidthDp);
         dest.writeInt(compatScreenHeightDp);
         dest.writeInt(compatSmallestScreenWidthDp);
+        dest.writeValue(appBounds);
         dest.writeInt(assetsSeq);
         dest.writeInt(seq);
     }
@@ -1529,6 +1561,7 @@
         compatScreenWidthDp = source.readInt();
         compatScreenHeightDp = source.readInt();
         compatSmallestScreenWidthDp = source.readInt();
+        appBounds = (Rect) source.readValue(null);
         assetsSeq = source.readInt();
         seq = source.readInt();
     }
@@ -1706,6 +1739,33 @@
     /**
      * @hide
      *
+     * Helper method for setting the app bounds.
+     */
+    public void setAppBounds(Rect rect) {
+        if (rect == null) {
+            appBounds = null;
+            return;
+        }
+
+        setAppBounds(rect.left, rect.top, rect.right, rect.bottom);
+    }
+
+    /**
+     * @hide
+     *
+     * Helper method for setting the app bounds.
+     */
+    public void setAppBounds(int left, int top, int right, int bottom) {
+        if (appBounds == null) {
+            appBounds = new Rect();
+        }
+
+        appBounds.set(left, top, right, bottom);
+    }
+
+    /**
+     * @hide
+     *
      * Clears the locale without changing layout direction.
      */
     public void clearLocales() {
@@ -2212,6 +2272,7 @@
     private static final String XML_ATTR_SCREEN_HEIGHT = "height";
     private static final String XML_ATTR_SMALLEST_WIDTH = "sw";
     private static final String XML_ATTR_DENSITY = "density";
+    private static final String XML_ATTR_APP_BOUNDS = "app_bounds";
 
     /**
      * Reads the attributes corresponding to Configuration member fields from the Xml parser.
@@ -2261,6 +2322,8 @@
                         SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
         configOut.densityDpi = XmlUtils.readIntAttribute(parser, XML_ATTR_DENSITY,
                 DENSITY_DPI_UNDEFINED);
+        configOut.appBounds =
+            Rect.unflattenFromString(XmlUtils.readStringAttribute(parser, XML_ATTR_APP_BOUNDS));
 
         // For persistence, we don't care about assetsSeq, so do not read it out.
     }
@@ -2332,6 +2395,11 @@
             XmlUtils.writeIntAttribute(xml, XML_ATTR_DENSITY, config.densityDpi);
         }
 
+        if (config.appBounds != null) {
+            XmlUtils.writeStringAttribute(xml, XML_ATTR_APP_BOUNDS,
+                config.appBounds.flattenToString());
+        }
+
         // For persistence, we do not care about assetsSeq, so do not write it out.
     }
 }
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 3d11dcb..0cec496 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -562,12 +562,10 @@
         outMetrics.xdpi = outMetrics.noncompatXdpi = physicalXDpi;
         outMetrics.ydpi = outMetrics.noncompatYdpi = physicalYDpi;
 
-        width = (configuration != null
-                && configuration.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED)
-                ? (int)((configuration.screenWidthDp * outMetrics.density) + 0.5f) : width;
-        height = (configuration != null
-                && configuration.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED)
-                ? (int)((configuration.screenHeightDp * outMetrics.density) + 0.5f) : height;
+        width = configuration != null && configuration.appBounds != null
+                ? configuration.appBounds.width() : width;
+        height = configuration != null && configuration.appBounds != null
+                ? configuration.appBounds.height() : height;
 
         outMetrics.noncompatWidthPixels  = outMetrics.widthPixels = width;
         outMetrics.noncompatHeightPixels = outMetrics.heightPixels = height;
diff --git a/graphics/java/android/graphics/Rect.java b/graphics/java/android/graphics/Rect.java
index 7f579a2..deafb66 100644
--- a/graphics/java/android/graphics/Rect.java
+++ b/graphics/java/android/graphics/Rect.java
@@ -20,6 +20,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import android.text.TextUtils;
 import java.io.PrintWriter;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -170,6 +171,10 @@
      * or null if the string is not of that form.
      */
     public static Rect unflattenFromString(String str) {
+        if (TextUtils.isEmpty(str)) {
+            return null;
+        }
+
         Matcher matcher = UnflattenHelper.getMatcher(str);
         if (!matcher.matches()) {
             return null;
@@ -179,7 +184,7 @@
                 Integer.parseInt(matcher.group(3)),
                 Integer.parseInt(matcher.group(4)));
     }
-    
+
     /**
      * Print short representation to given writer.
      * @hide
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index 47e11b1..432988a 100644
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -2122,23 +2122,31 @@
         return true;
     }
 
-    /** Computes the override configuration for this activity */
+    /**
+     * Computes the bounds to fit the Activity within the bounds of the {@link Configuration}.
+     */
     // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer.
     private void computeBounds(Rect outBounds) {
         outBounds.setEmpty();
         final float maxAspectRatio = info.maxAspectRatio;
         final ActivityStack stack = getStack();
-        if ((task != null && !task.mFullscreen) || maxAspectRatio == 0 || stack == null) {
+        if (task == null || stack == null || !task.mFullscreen || maxAspectRatio == 0) {
             // We don't set override configuration if that activity task isn't fullscreen. I.e. the
             // activity is in multi-window mode. Or, there isn't a max aspect ratio specified for
-            // the activity.
+            // the activity. This is indicated by an empty {@link outBounds}.
             return;
         }
 
-        stack.getDisplaySize(mTmpPoint);
-        int maxActivityWidth = mTmpPoint.x;
-        int maxActivityHeight = mTmpPoint.y;
-        if (mTmpPoint.x < mTmpPoint.y) {
+        // We must base this on the parent configuration, because we set our override
+        // configuration's appBounds based on the result of this method. If we used our own
+        // configuration, it would be influenced by past invocations.
+        final Configuration configuration = getParent().getConfiguration();
+        final int containingAppWidth = configuration.appBounds.width();
+        final int containingAppHeight = configuration.appBounds.height();
+        int maxActivityWidth = containingAppWidth;
+        int maxActivityHeight = containingAppHeight;
+
+        if (containingAppWidth < containingAppHeight) {
             // Width is the shorter side, so we use that to figure-out what the max. height should
             // be given the aspect ratio.
             maxActivityHeight = (int) ((maxActivityWidth * maxAspectRatio) + 0.5f);
@@ -2148,8 +2156,14 @@
             maxActivityWidth = (int) ((maxActivityHeight * maxAspectRatio) + 0.5f);
         }
 
-        if (mTmpPoint.x <= maxActivityWidth && mTmpPoint.y <= maxActivityHeight) {
+        if (containingAppWidth <= maxActivityWidth && containingAppHeight <= maxActivityHeight) {
             // The display matches or is less than the activity aspect ratio, so nothing else to do.
+            // Return the existing bounds. If this method is running for the first time,
+            // {@link mBounds} will be empty (representing no override). If the method has run
+            // before, then effect of {@link mBounds} will already have been applied to the
+            // value returned from {@link getConfiguration}. Refer to
+            // {@link TaskRecord#computeOverrideConfiguration}.
+            outBounds.set(mBounds);
             return;
         }
 
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index ce32f841..ee4a42d 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -2044,6 +2044,7 @@
 
         config.unset();
         final Configuration parentConfig = getParent().getConfiguration();
+
         final float density = parentConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
 
         if (mStack != null) {
@@ -2052,11 +2053,7 @@
                     mTmpNonDecorBounds, mTmpStableBounds, overrideWidth, overrideHeight, density,
                     config, parentConfig);
         } else {
-            // No stack, give some default values
-            config.smallestScreenWidthDp =
-                    mService.mStackSupervisor.mDefaultMinSizeOfResizeableTask;
-            config.screenWidthDp = config.screenHeightDp = config.smallestScreenWidthDp;
-            Slog.wtf(TAG, "Expected stack when calculating override config");
+            throw new IllegalArgumentException("Expected stack when calculating override config");
         }
 
         config.orientation = (config.screenWidthDp <= config.screenHeightDp)
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ae413e5..3c68e4f 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1134,6 +1134,13 @@
         config.screenHeightDp =
                 (int)(mService.mPolicy.getConfigDisplayHeight(dw, dh, displayInfo.rotation,
                         config.uiMode, mDisplayId) / mDisplayMetrics.density);
+
+        mService.mPolicy.getNonDecorInsetsLw(displayInfo.rotation, dw, dh, mTmpRect);
+        final int leftInset = mTmpRect.left;
+        final int topInset = mTmpRect.top;
+        // appBounds at the root level should mirror the app screen size.
+        config.setAppBounds(leftInset /*left*/, topInset /*top*/, leftInset + displayInfo.appWidth /*right*/,
+                topInset + displayInfo.appHeight /*bottom*/);
         final boolean rotated = (displayInfo.rotation == Surface.ROTATION_90
                 || displayInfo.rotation == Surface.ROTATION_270);
 
diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java
index 85eae02..e300256 100644
--- a/services/core/java/com/android/server/wm/DockedStackDividerController.java
+++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java
@@ -225,14 +225,25 @@
             mService.mPolicy.getStableInsetsLw(rotation, dw, dh, mTmpRect);
             config.unset();
             config.orientation = (dw <= dh) ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+
+            final int displayId = mDisplayContent.getDisplayId();
+            final int appWidth = mService.mPolicy.getNonDecorDisplayWidth(dw, dh, rotation,
+                baseConfig.uiMode, displayId);
+            final int appHeight = mService.mPolicy.getNonDecorDisplayHeight(dw, dh, rotation,
+                baseConfig.uiMode, displayId);
+            mService.mPolicy.getNonDecorInsetsLw(rotation, dw, dh, mTmpRect);
+            final int leftInset = mTmpRect.left;
+            final int topInset = mTmpRect.top;
+
+            config.setAppBounds(leftInset /*left*/, topInset /*top*/, leftInset + appWidth /*right*/,
+                    topInset + appHeight /*bottom*/);
+
             config.screenWidthDp = (int)
                     (mService.mPolicy.getConfigDisplayWidth(dw, dh, rotation, baseConfig.uiMode,
-                            mDisplayContent.getDisplayId()) /
-                            mDisplayContent.getDisplayMetrics().density);
+                            displayId) / mDisplayContent.getDisplayMetrics().density);
             config.screenHeightDp = (int)
                     (mService.mPolicy.getConfigDisplayHeight(dw, dh, rotation, baseConfig.uiMode,
-                            mDisplayContent.getDisplayId()) /
-                            mDisplayContent.getDisplayMetrics().density);
+                            displayId) / mDisplayContent.getDisplayMetrics().density);
             final Context rotationContext = mService.mContext.createConfigurationContext(config);
             mSnapAlgorithmForRotation[rotation] = new DividerSnapAlgorithm(
                     rotationContext.getResources(), dw, dh, getContentWidth(),
diff --git a/services/core/java/com/android/server/wm/StackWindowController.java b/services/core/java/com/android/server/wm/StackWindowController.java
index 8186d30..b2029dd 100644
--- a/services/core/java/com/android/server/wm/StackWindowController.java
+++ b/services/core/java/com/android/server/wm/StackWindowController.java
@@ -270,6 +270,12 @@
 
             int width;
             int height;
+
+            final Rect parentAppBounds = parentConfig.appBounds;
+
+            config.setAppBounds(!bounds.isEmpty() ? bounds : null);
+            boolean intersectParentBounds = false;
+
             if (StackId.tasksAreFloating(mStackId)) {
                 // Floating tasks should not be resized to the screen's bounds.
 
@@ -280,6 +286,7 @@
                     // the fullscreen stack, without intersecting it with the display bounds
                     stableBounds.inset(mTmpStableInsets);
                     nonDecorBounds.inset(mTmpNonDecorInsets);
+                    intersectParentBounds = true;
                 }
                 width = (int) (stableBounds.width() / density);
                 height = (int) (stableBounds.height() / density);
@@ -299,6 +306,11 @@
                         parentConfig.screenWidthDp);
                 height = Math.min((int) (stableBounds.height() / density),
                         parentConfig.screenHeightDp);
+                intersectParentBounds = true;
+            }
+
+            if (intersectParentBounds && config.appBounds != null) {
+                config.appBounds.intersect(parentAppBounds);
             }
 
             config.screenWidthDp = width;
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppBoundsTests.java b/services/tests/servicestests/src/com/android/server/wm/AppBoundsTests.java
new file mode 100644
index 0000000..f68e06a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppBoundsTests.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 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 com.android.server.wm;
+
+import android.app.ActivityManager;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Debug;
+import android.view.DisplayInfo;
+import org.junit.Test;
+
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class to exercise logic related to {@link android.content.res.Configuration#appBounds}.
+ *
+ * Build/Install/Run:
+ *  bit FrameworksServicesTests:com.android.server.wm.AppBoundsTests
+ */
+@SmallTest
+@Presubmit
+@org.junit.runner.RunWith(AndroidJUnit4.class)
+public class AppBoundsTests extends WindowTestsBase {
+    private Rect mParentBounds;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mParentBounds = new Rect(10 /*left*/, 30 /*top*/, 80 /*right*/, 60 /*bottom*/);
+    }
+
+    /**
+     * Ensures the configuration app bounds at the root level match the app dimensions.
+     */
+    @Test
+    public void testRootConfigurationBounds() throws Exception {
+        final DisplayInfo info = sDisplayContent.getDisplayInfo();
+        info.appWidth = 1024;
+        info.appHeight = 768;
+
+        final Configuration config = sWm.computeNewConfiguration(sDisplayContent.getDisplayId());
+        // The bounds should always be positioned in the top left.
+        assertEquals(config.appBounds.left, 0);
+        assertEquals(config.appBounds.top, 0);
+
+        // The bounds should equal the defined app width and height
+        assertEquals(config.appBounds.width(), info.appWidth);
+        assertEquals(config.appBounds.height(), info.appHeight);
+    }
+
+    /**
+     * Ensures that bounds are clipped to their parent.
+     */
+    @Test
+    public void testBoundsClipping() throws Exception {
+        final Rect shiftedBounds = new Rect(mParentBounds);
+        shiftedBounds.offset(10, 10);
+        final Rect expectedBounds = new Rect(mParentBounds);
+        expectedBounds.intersect(shiftedBounds);
+        testStackBoundsConfiguration(null /*stackId*/, mParentBounds, shiftedBounds,
+                expectedBounds);
+    }
+
+    /**
+     * Ensures that empty bounds are not propagated to the configuration.
+     */
+    @Test
+    public void testEmptyBounds() throws Exception {
+        final Rect emptyBounds = new Rect();
+        testStackBoundsConfiguration(null /*stackId*/, mParentBounds, emptyBounds,
+                null /*ExpectedBounds*/);
+    }
+
+    /**
+     * Ensures that bounds on freeform stacks are not clipped.
+     */
+    @Test
+    public void testFreeFormBounds() throws Exception {
+        final Rect freeFormBounds = new Rect(mParentBounds);
+        freeFormBounds.offset(10, 10);
+        testStackBoundsConfiguration(ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID,
+                mParentBounds, freeFormBounds, freeFormBounds);
+    }
+
+    /**
+     * Ensures that fully contained bounds are not clipped.
+     */
+    @Test
+    public void testContainedBounds() throws Exception {
+        final Rect insetBounds = new Rect(mParentBounds);
+        insetBounds.inset(5, 5, 5, 5);
+        testStackBoundsConfiguration(null /*stackId*/, mParentBounds, insetBounds, insetBounds);
+    }
+
+    /**
+     * Ensures that full screen free form bounds are clipped
+     */
+    @Test
+    public void testFullScreenFreeFormBounds() throws Exception {
+        final Rect fullScreenBounds = new Rect(0, 0, sDisplayInfo.logicalWidth,
+                sDisplayInfo.logicalHeight);
+        testStackBoundsConfiguration(null /*stackId*/, mParentBounds, fullScreenBounds,
+                mParentBounds);
+    }
+
+
+    private void testStackBoundsConfiguration(Integer stackId, Rect parentBounds, Rect bounds,
+            Rect expectedConfigBounds) {
+        final StackWindowController stackController = stackId != null ?
+                createStackControllerOnStackOnDisplay(stackId, sDisplayContent)
+                : createStackControllerOnDisplay(sDisplayContent);
+
+        final Configuration parentConfig = sDisplayContent.getConfiguration();
+        parentConfig.setAppBounds(parentBounds);
+
+        final Configuration config = new Configuration();
+        stackController.adjustConfigurationForBounds(bounds, null /*insetBounds*/,
+                new Rect() /*nonDecorBounds*/, new Rect() /*stableBounds*/, false /*overrideWidth*/,
+                false /*overrideHeight*/, sDisplayInfo.logicalDensityDpi, config, parentConfig);
+        // Assert that both expected and actual are null or are equal to each other
+
+        assertTrue((expectedConfigBounds == null && config.appBounds == null)
+                || expectedConfigBounds.equals(config.appBounds));
+    }
+}