diff --git a/core/java/android/window/SizeConfigurationBuckets.java b/core/java/android/window/SizeConfigurationBuckets.java index 7422f2449a8d4..f474f0a76cc6d 100644 --- a/core/java/android/window/SizeConfigurationBuckets.java +++ b/core/java/android/window/SizeConfigurationBuckets.java @@ -16,6 +16,7 @@ package android.window; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; @@ -25,6 +26,7 @@ import android.content.res.Configuration; import android.os.Parcelable; import android.util.SparseIntArray; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DataClass; import java.util.Arrays; @@ -54,10 +56,24 @@ public final class SizeConfigurationBuckets implements Parcelable { @Nullable private final int[] mSmallest; + /** Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets */ + @Nullable + private final int[] mScreenLayoutSize; + + /** + * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a + * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and + * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change. + */ + private final boolean mScreenLayoutLongSet; + public SizeConfigurationBuckets(Configuration[] sizeConfigurations) { SparseIntArray horizontal = new SparseIntArray(); SparseIntArray vertical = new SparseIntArray(); SparseIntArray smallest = new SparseIntArray(); + SparseIntArray screenLayoutSize = new SparseIntArray(); + int curScreenLayoutSize; + boolean screenLayoutLongSet = false; for (int i = sizeConfigurations.length - 1; i >= 0; i--) { Configuration config = sizeConfigurations[i]; if (config.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { @@ -69,23 +85,42 @@ public final class SizeConfigurationBuckets implements Parcelable { if (config.smallestScreenWidthDp != Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) { smallest.put(config.smallestScreenWidthDp, 0); } + if ((curScreenLayoutSize = config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) + != Configuration.SCREENLAYOUT_SIZE_UNDEFINED) { + screenLayoutSize.put(curScreenLayoutSize, 0); + } + if (!screenLayoutLongSet && (config.screenLayout & Configuration.SCREENLAYOUT_LONG_MASK) + != Configuration.SCREENLAYOUT_LONG_UNDEFINED) { + screenLayoutLongSet = true; + } } mHorizontal = horizontal.copyKeys(); mVertical = vertical.copyKeys(); mSmallest = smallest.copyKeys(); + mScreenLayoutSize = screenLayoutSize.copyKeys(); + mScreenLayoutLongSet = screenLayoutLongSet; } /** * Get the changes between two configurations but don't count changes in sizes if they don't - * cross boundaries that are important to the app. + * cross boundaries that are important to the app. * * This is a static helper to deal with null `buckets`. When no buckets have been specified, * this actually filters out all 3 size-configs. This is legacy behavior. */ - public static int filterDiff(int diff, Configuration oldConfig, Configuration newConfig, - @Nullable SizeConfigurationBuckets buckets) { + public static int filterDiff(int diff, @NonNull Configuration oldConfig, + @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) { + final boolean nonSizeLayoutFieldsUnchanged = + areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout); if (buckets == null) { - return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE); + // Only unflip CONFIG_SCREEN_LAYOUT if non-size-related attributes of screen layout do + // not change. + if (nonSizeLayoutFieldsUnchanged) { + return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE + | CONFIG_SCREEN_LAYOUT); + } else { + return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE); + } } if ((diff & CONFIG_SCREEN_SIZE) != 0) { final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp, @@ -103,6 +138,13 @@ public final class SizeConfigurationBuckets implements Parcelable { diff &= ~CONFIG_SMALLEST_SCREEN_SIZE; } } + if ((diff & CONFIG_SCREEN_LAYOUT) != 0 && nonSizeLayoutFieldsUnchanged) { + if (!buckets.crossesScreenLayoutSizeThreshold(oldConfig, newConfig) + && !buckets.crossesScreenLayoutLongThreshold(oldConfig.screenLayout, + newConfig.screenLayout)) { + diff &= ~CONFIG_SCREEN_LAYOUT; + } + } return diff; } @@ -118,6 +160,61 @@ public final class SizeConfigurationBuckets implements Parcelable { return crossesSizeThreshold(mSmallest, firstDp, secondDp); } + /** + * Returns whether a screen layout size threshold has been crossed. + */ + @VisibleForTesting + public boolean crossesScreenLayoutSizeThreshold(@NonNull Configuration firstConfig, + @NonNull Configuration secondConfig) { + // If both the old and new screen layout are equal (both can be undefined), then no + // threshold is crossed. + if ((firstConfig.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) + == (secondConfig.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)) { + return false; + } + // Any time the new layout size is smaller than the old layout size, the activity has + // crossed a size threshold because layout size represents the smallest possible size the + // activity can occupy. + if (!secondConfig.isLayoutSizeAtLeast(firstConfig.screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK)) { + return true; + } + // If the new layout size is at least as large as the old layout size, then check if the new + // layout size has crossed a threshold. + if (mScreenLayoutSize != null) { + for (int screenLayoutSize : mScreenLayoutSize) { + if (firstConfig.isLayoutSizeAtLeast(screenLayoutSize) + != secondConfig.isLayoutSizeAtLeast(screenLayoutSize)) { + return true; + } + } + } + return false; + } + + private boolean crossesScreenLayoutLongThreshold(int firstScreenLayout, + int secondScreenLayout) { + final int firstScreenLayoutLongValue = firstScreenLayout + & Configuration.SCREENLAYOUT_LONG_MASK; + final int secondScreenLayoutLongValue = secondScreenLayout + & Configuration.SCREENLAYOUT_LONG_MASK; + return mScreenLayoutLongSet && firstScreenLayoutLongValue != secondScreenLayoutLongValue; + } + + /** + * Returns whether non-size related screen layout attributes have changed. If true, then + * {@link ActivityInfo#CONFIG_SCREEN_LAYOUT} should not be filtered out in + * {@link SizeConfigurationBuckets#filterDiff()} because the non-size related attributes + * do not have a bucket range like the size-related attributes of screen layout. + */ + @VisibleForTesting + public static boolean areNonSizeLayoutFieldsUnchanged(int oldScreenLayout, + int newScreenLayout) { + final int nonSizeRelatedFields = Configuration.SCREENLAYOUT_LAYOUTDIR_MASK + | Configuration.SCREENLAYOUT_ROUND_MASK | Configuration.SCREENLAYOUT_COMPAT_NEEDED; + return (oldScreenLayout & nonSizeRelatedFields) == (newScreenLayout & nonSizeRelatedFields); + } + /** * The purpose of this method is to decide whether the activity needs to be relaunched upon * changing its size. In most cases the activities don't need to be relaunched, if the resize @@ -132,7 +229,8 @@ public final class SizeConfigurationBuckets implements Parcelable { * it resizes width from 620dp to 700dp, it won't be relaunched as it stays on the same side * of the threshold. */ - private static boolean crossesSizeThreshold(int[] thresholds, int firstDp, + @VisibleForTesting + public static boolean crossesSizeThreshold(int[] thresholds, int firstDp, int secondDp) { if (thresholds == null) { return false; @@ -150,12 +248,13 @@ public final class SizeConfigurationBuckets implements Parcelable { @Override public String toString() { return Arrays.toString(mHorizontal) + " " + Arrays.toString(mVertical) + " " - + Arrays.toString(mSmallest); + + Arrays.toString(mSmallest) + " " + Arrays.toString(mScreenLayoutSize) + " " + + mScreenLayoutLongSet; } - // Code below generated by codegen v1.0.22. + // Code below generated by codegen v1.0.23. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -177,15 +276,25 @@ public final class SizeConfigurationBuckets implements Parcelable { * Vertical (screenHeightDp) buckets * @param smallest * Smallest (smallestScreenWidthDp) buckets + * @param screenLayoutSize + * Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets + * @param screenLayoutLongSet + * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a + * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and + * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change. */ @DataClass.Generated.Member public SizeConfigurationBuckets( @Nullable int[] horizontal, @Nullable int[] vertical, - @Nullable int[] smallest) { + @Nullable int[] smallest, + @Nullable int[] screenLayoutSize, + boolean screenLayoutLongSet) { this.mHorizontal = horizontal; this.mVertical = vertical; this.mSmallest = smallest; + this.mScreenLayoutSize = screenLayoutSize; + this.mScreenLayoutLongSet = screenLayoutLongSet; // onConstructed(); // You can define this method to get a callback } @@ -214,6 +323,24 @@ public final class SizeConfigurationBuckets implements Parcelable { return mSmallest; } + /** + * Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets + */ + @DataClass.Generated.Member + public @Nullable int[] getScreenLayoutSize() { + return mScreenLayoutSize; + } + + /** + * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a + * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and + * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change. + */ + @DataClass.Generated.Member + public boolean isScreenLayoutLongSet() { + return mScreenLayoutLongSet; + } + @Override @DataClass.Generated.Member public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { @@ -221,13 +348,16 @@ public final class SizeConfigurationBuckets implements Parcelable { // void parcelFieldName(Parcel dest, int flags) { ... } byte flg = 0; + if (mScreenLayoutLongSet) flg |= 0x10; if (mHorizontal != null) flg |= 0x1; if (mVertical != null) flg |= 0x2; if (mSmallest != null) flg |= 0x4; + if (mScreenLayoutSize != null) flg |= 0x8; dest.writeByte(flg); if (mHorizontal != null) dest.writeIntArray(mHorizontal); if (mVertical != null) dest.writeIntArray(mVertical); if (mSmallest != null) dest.writeIntArray(mSmallest); + if (mScreenLayoutSize != null) dest.writeIntArray(mScreenLayoutSize); } @Override @@ -242,13 +372,17 @@ public final class SizeConfigurationBuckets implements Parcelable { // static FieldType unparcelFieldName(Parcel in) { ... } byte flg = in.readByte(); + boolean screenLayoutLongSet = (flg & 0x10) != 0; int[] horizontal = (flg & 0x1) == 0 ? null : in.createIntArray(); int[] vertical = (flg & 0x2) == 0 ? null : in.createIntArray(); int[] smallest = (flg & 0x4) == 0 ? null : in.createIntArray(); + int[] screenLayoutSize = (flg & 0x8) == 0 ? null : in.createIntArray(); this.mHorizontal = horizontal; this.mVertical = vertical; this.mSmallest = smallest; + this.mScreenLayoutSize = screenLayoutSize; + this.mScreenLayoutLongSet = screenLayoutLongSet; // onConstructed(); // You can define this method to get a callback } @@ -268,10 +402,10 @@ public final class SizeConfigurationBuckets implements Parcelable { }; @DataClass.Generated( - time = 1615845864280L, - codegenVersion = "1.0.22", + time = 1628273704583L, + codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/window/SizeConfigurationBuckets.java", - inputSignatures = "private final @android.annotation.Nullable int[] mHorizontal\nprivate final @android.annotation.Nullable int[] mVertical\nprivate final @android.annotation.Nullable int[] mSmallest\npublic static int filterDiff(int,android.content.res.Configuration,android.content.res.Configuration,android.window.SizeConfigurationBuckets)\nprivate boolean crossesHorizontalSizeThreshold(int,int)\nprivate boolean crossesVerticalSizeThreshold(int,int)\nprivate boolean crossesSmallestSizeThreshold(int,int)\nprivate static boolean crossesSizeThreshold(int[],int,int)\npublic @java.lang.Override java.lang.String toString()\nclass SizeConfigurationBuckets extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genAidl=true)") + inputSignatures = "private final @android.annotation.Nullable int[] mHorizontal\nprivate final @android.annotation.Nullable int[] mVertical\nprivate final @android.annotation.Nullable int[] mSmallest\nprivate final @android.annotation.Nullable int[] mScreenLayoutSize\nprivate final boolean mScreenLayoutLongSet\npublic static int filterDiff(int,android.content.res.Configuration,android.content.res.Configuration,android.window.SizeConfigurationBuckets)\nprivate boolean crossesHorizontalSizeThreshold(int,int)\nprivate boolean crossesVerticalSizeThreshold(int,int)\nprivate boolean crossesSmallestSizeThreshold(int,int)\npublic @com.android.internal.annotations.VisibleForTesting boolean crossesScreenLayoutSizeThreshold(android.content.res.Configuration,android.content.res.Configuration)\nprivate boolean crossesScreenLayoutLongThreshold(int,int)\npublic static @com.android.internal.annotations.VisibleForTesting boolean areNonSizeLayoutFieldsUnchanged(int,int)\npublic static @com.android.internal.annotations.VisibleForTesting boolean crossesSizeThreshold(int[],int,int)\npublic @java.lang.Override java.lang.String toString()\nclass SizeConfigurationBuckets extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genAidl=true)") @Deprecated private void __metadata() {} diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index e93b00d7b148b..86d781033e5e3 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -92,6 +92,7 @@ static struct configuration_offsets_t { jfieldID mSmallestScreenWidthDpOffset; jfieldID mScreenWidthDpOffset; jfieldID mScreenHeightDpOffset; + jfieldID mScreenLayoutOffset; } gConfigurationOffsets; static struct arraymap_offsets_t { @@ -1019,6 +1020,7 @@ static jobject ConstructConfigurationObject(JNIEnv* env, const ResTable_config& config.smallestScreenWidthDp); env->SetIntField(result, gConfigurationOffsets.mScreenWidthDpOffset, config.screenWidthDp); env->SetIntField(result, gConfigurationOffsets.mScreenHeightDpOffset, config.screenHeightDp); + env->SetIntField(result, gConfigurationOffsets.mScreenLayoutOffset, config.screenLayout); return result; } @@ -1553,6 +1555,8 @@ int register_android_content_AssetManager(JNIEnv* env) { GetFieldIDOrDie(env, configurationClass, "screenWidthDp", "I"); gConfigurationOffsets.mScreenHeightDpOffset = GetFieldIDOrDie(env, configurationClass, "screenHeightDp", "I"); + gConfigurationOffsets.mScreenLayoutOffset = + GetFieldIDOrDie(env, configurationClass, "screenLayout", "I"); jclass arrayMapClass = FindClassOrDie(env, "android/util/ArrayMap"); gArrayMapOffsets.classObject = MakeGlobalRefOrDie(env, arrayMapClass); diff --git a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java new file mode 100644 index 0000000000000..fa4aa803c75ee --- /dev/null +++ b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2021 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 android.window; + +import static android.content.pm.ActivityInfo.CONFIG_LOCALE; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; +import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; +import static android.content.res.Configuration.SCREENLAYOUT_COMPAT_NEEDED; +import static android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_LTR; +import static android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; +import static android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_UNDEFINED; +import static android.content.res.Configuration.SCREENLAYOUT_LONG_NO; +import static android.content.res.Configuration.SCREENLAYOUT_LONG_YES; +import static android.content.res.Configuration.SCREENLAYOUT_ROUND_NO; +import static android.content.res.Configuration.SCREENLAYOUT_ROUND_UNDEFINED; +import static android.content.res.Configuration.SCREENLAYOUT_ROUND_YES; +import static android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE; +import static android.content.res.Configuration.SCREENLAYOUT_SIZE_NORMAL; +import static android.content.res.Configuration.SCREENLAYOUT_SIZE_SMALL; +import static android.content.res.Configuration.SCREENLAYOUT_SIZE_XLARGE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.res.Configuration; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +/** + * Tests for {@link SizeConfigurationBuckets} + * + * Build/Install/Run: + * atest FrameworksMockingCoreTests:SizeConfigurationBucketsTest + */ + +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class SizeConfigurationBucketsTest { + + /** + * Tests that a change in any of the non-size-related screen layout fields results in + * {@link SizeConfigurationBuckets#areNonSizeLayoutFieldsUnchanged} returning false. + */ + @Test + public void testNonSizeRelatedScreenLayoutFields() { + // Test layout direction + assertEquals(true, SizeConfigurationBuckets + .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_LAYOUTDIR_UNDEFINED)); + assertEquals(false, SizeConfigurationBuckets + .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_LAYOUTDIR_LTR)); + assertEquals(false, SizeConfigurationBuckets + .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_LAYOUTDIR_RTL)); + + // Test layout roundness + assertEquals(true, SizeConfigurationBuckets + .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_ROUND_UNDEFINED)); + assertEquals(false, SizeConfigurationBuckets + .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_ROUND_NO)); + assertEquals(false, SizeConfigurationBuckets + .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_ROUND_YES)); + + // Test layout compat needed + assertEquals(false, SizeConfigurationBuckets + .areNonSizeLayoutFieldsUnchanged(0, SCREENLAYOUT_COMPAT_NEEDED)); + } + + /** + * Tests that null size configuration buckets unflips the correct configuration flags. + */ + @Test + public void testNullSizeConfigurationBuckets() { + // Check that all 3 size configurations are filtered out of the diff if the buckets are null + // and non-size attributes of screen layout are unchanged. Add a non-size related config + // change (i.e. CONFIG_LOCALE) to test that the diff is not set to zero. + final int diff = CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE | CONFIG_SCREEN_LAYOUT + | CONFIG_LOCALE; + final int filteredDiffNonSizeLayoutUnchanged = SizeConfigurationBuckets.filterDiff(diff, + Configuration.EMPTY, Configuration.EMPTY, null); + assertEquals(CONFIG_LOCALE, filteredDiffNonSizeLayoutUnchanged); + + // Check that only screen size and smallest screen size are filtered out of the diff if the + // buckets are null and non-size attributes of screen layout are changed. + final Configuration newConfig = new Configuration(); + newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES; + final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff, + Configuration.EMPTY, newConfig, null); + assertEquals(CONFIG_SCREEN_LAYOUT | CONFIG_LOCALE, filteredDiffNonSizeLayoutChanged); + } + + /** + * Tests that {@link SizeConfigurationBuckets.crossesSizeThreshold()} correctly checks whether + * the bucket thresholds have or have not been crossed. This test includes boundary checks + * to ensure that arithmetic is inclusive and exclusive in the right places. + */ + @Test + public void testCrossesSizeThreshold() { + final int[] thresholds = new int[] { 360, 600 }; + final int nThresholds = thresholds.length; + for (int i = -1; i < nThresholds; i++) { + final int minValueInBucket = i < 0 ? 0 : thresholds[i]; + final int maxValueInBucket = i < nThresholds - 1 + ? thresholds[i + 1] - 1 : Integer.MAX_VALUE; + final int bucketRange = maxValueInBucket - minValueInBucket; + // Set old value to 1/4 in between the two thresholds. + final int oldValue = (int) (minValueInBucket + bucketRange * 0.25); + // Test 3 values of new value spread across bucket range: minValueInBucket, bucket + // midpoint, and max value in bucket. In all 3 cases, the bucket has not changed so + // {@link SizeConfigurationBuckets#crossedSizeThreshold()} should return false. + checkCrossesSizeThreshold(thresholds, oldValue, minValueInBucket, false); + checkCrossesSizeThreshold(thresholds, oldValue, + (int) (minValueInBucket + bucketRange * 0.5), false); + checkCrossesSizeThreshold(thresholds, oldValue, maxValueInBucket, false); + // Test 4 values of size spread outside of bucket range: more than 1 less than min + // value, 1 less than min value, 1 more than max value, and more than 1 more than max + // value. In all 4 cases, the bucket has changed so + // {@link SizeConfigurationBuckets#crossedSizeThreshold()} should return true. + // Only test less than min value if min value > 0. + if (minValueInBucket > 0) { + checkCrossesSizeThreshold(thresholds, oldValue, minValueInBucket - 20, true); + checkCrossesSizeThreshold(thresholds, oldValue, minValueInBucket - 1, true); + } + // Only test greater than max value if not in highest bucket. + if (i < nThresholds - 1) { + checkCrossesSizeThreshold(thresholds, oldValue, maxValueInBucket + 1, true); + checkCrossesSizeThreshold(thresholds, oldValue, maxValueInBucket + 20, true); + } + } + } + + /** + * Tests that if screen layout size changed but did not cross a threshold, the filtered diff + * does not include screen layout. + */ + @Test + public void testScreenLayoutFilteredIfSizeDidNotCrossThreshold() { + // Set only small and large sizes + final Configuration[] sizeConfigs = new Configuration[2]; + sizeConfigs[0] = new Configuration(); + sizeConfigs[0].screenLayout |= SCREENLAYOUT_SIZE_SMALL; + sizeConfigs[1] = new Configuration(); + sizeConfigs[1].screenLayout |= SCREENLAYOUT_SIZE_LARGE; + final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(sizeConfigs); + + // Change screen layout size from small to normal and check that screen layout flag is + // not part of the diff because a threshold was not crossed. + final int diff = CONFIG_SCREEN_LAYOUT; + final Configuration oldConfig = new Configuration(); + oldConfig.screenLayout |= SCREENLAYOUT_SIZE_SMALL; + final Configuration newConfig = new Configuration(); + newConfig.screenLayout |= SCREENLAYOUT_SIZE_NORMAL; + final int filteredDiff = SizeConfigurationBuckets.filterDiff(diff, oldConfig, newConfig, + sizeBuckets); + assertEquals(0, filteredDiff); + + // If a non-size attribute of screen layout changed, then screen layout should not be + // filtered from the diff. + newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES; + final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff, + oldConfig, newConfig, sizeBuckets); + assertEquals(CONFIG_SCREEN_LAYOUT, filteredDiffNonSizeLayoutChanged); + } + + /** + * Tests that if screen layout size changed and did cross a threshold, the filtered diff + * includes screen layout. + */ + @Test + public void testScreenLayoutNotFilteredIfSizeCrossedThreshold() { + // Set only small and normal sizes + final Configuration[] sizeConfigs = new Configuration[2]; + sizeConfigs[0] = new Configuration(); + sizeConfigs[0].screenLayout |= SCREENLAYOUT_SIZE_SMALL; + sizeConfigs[1] = new Configuration(); + sizeConfigs[1].screenLayout |= SCREENLAYOUT_SIZE_NORMAL; + final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(sizeConfigs); + + // Change screen layout size from small to normal and check that screen layout flag is + // still part of the diff because a threshold was crossed. + final int diff = CONFIG_SCREEN_LAYOUT; + final Configuration oldConfig = new Configuration(); + oldConfig.screenLayout |= SCREENLAYOUT_SIZE_SMALL; + final Configuration newConfig = new Configuration(); + newConfig.screenLayout |= SCREENLAYOUT_SIZE_NORMAL; + final int filteredDiff = SizeConfigurationBuckets.filterDiff(diff, oldConfig, newConfig, + sizeBuckets); + assertEquals(CONFIG_SCREEN_LAYOUT, filteredDiff); + } + + /** + * Tests that anytime screen layout size is decreased, the filtered diff still includes screen + * layout. + */ + @Test + public void testScreenLayoutNotFilteredIfSizeDecreased() { + // The size thresholds can be anything, but can't be null + final int[] horizontalThresholds = new int[] { 360, 600 }; + final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets( + horizontalThresholds, null /* vertical */, null /* smallest */, + null /* screenLayoutSize */, false /* screenLayoutLongSet */); + final int[] sizeValuesInOrder = new int[] { + SCREENLAYOUT_SIZE_SMALL, SCREENLAYOUT_SIZE_NORMAL, SCREENLAYOUT_SIZE_LARGE, + SCREENLAYOUT_SIZE_XLARGE + }; + final int nSizes = sizeValuesInOrder.length; + for (int larger = nSizes - 1; larger > 0; larger--) { + for (int smaller = larger - 1; smaller >= 0; smaller--) { + final Configuration oldConfig = new Configuration(); + oldConfig.screenLayout |= sizeValuesInOrder[larger]; + final Configuration newConfig = new Configuration(); + newConfig.screenLayout |= sizeValuesInOrder[smaller]; + assertTrue(String.format("oldSize=%d, newSize=%d", oldConfig.screenLayout, + newConfig.screenLayout), + sizeBuckets.crossesScreenLayoutSizeThreshold(oldConfig, newConfig)); + } + } + } + + /** + * Tests that if screen layout long changed but did not cross a threshold, the filtered diff + * does not include screen layout. + */ + @Test + public void testScreenLayoutFilteredIfLongDidNotCrossThreshold() { + // Do not set any long threshold + final Configuration[] sizeConfigs = new Configuration[1]; + sizeConfigs[0] = Configuration.EMPTY; + final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(sizeConfigs); + + // Change screen layout long from not long to long and check that screen layout flag is + // not part of the diff because a threshold was not crossed. + final int diff = CONFIG_SCREEN_LAYOUT; + final Configuration oldConfig = new Configuration(); + oldConfig.screenLayout |= SCREENLAYOUT_LONG_NO; + final Configuration newConfig = new Configuration(); + newConfig.screenLayout |= SCREENLAYOUT_LONG_YES; + final int filteredDiff = SizeConfigurationBuckets.filterDiff(diff, oldConfig, newConfig, + sizeBuckets); + assertEquals(0, filteredDiff); + + // If a non-size attribute of screen layout changed, then screen layout should not be + // filtered from the diff. + newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES; + final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff, + oldConfig, newConfig, sizeBuckets); + assertEquals(CONFIG_SCREEN_LAYOUT, filteredDiffNonSizeLayoutChanged); + } + + /** + * Tests that if screen layout long changed and did cross a threshold, the filtered diff + * includes screen layout. + */ + @Test + public void testScreenLayoutNotFilteredIfLongCrossedThreshold() { + // Set only small and normal sizes + final Configuration[] sizeConfigs = new Configuration[1]; + sizeConfigs[0] = new Configuration(); + sizeConfigs[0].screenLayout |= SCREENLAYOUT_LONG_NO; + final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets(sizeConfigs); + + // Change screen layout long from not long to long and check that screen layout flag is + // still part of the diff because a threshold was crossed. + final int diff = CONFIG_SCREEN_LAYOUT; + final Configuration oldConfig = new Configuration(); + oldConfig.screenLayout |= SCREENLAYOUT_LONG_NO; + final Configuration newConfig = new Configuration(); + newConfig.screenLayout |= SCREENLAYOUT_LONG_YES; + final int filteredDiff = SizeConfigurationBuckets.filterDiff(diff, oldConfig, newConfig, + sizeBuckets); + assertEquals(CONFIG_SCREEN_LAYOUT, filteredDiff); + } + + /** + * Tests that horizontal buckets are correctly checked in + * {@link SizeConfigurationBuckets#filterDiff()}. + */ + @Test + public void testHorizontalSizeThresholds() { + final int[] horizontalThresholds = new int[] { 360, 600 }; + final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets( + horizontalThresholds, null /* vertical */, null /* smallest */, + null /* screenLayoutSize */, false /* screenLayoutLongSet */); + + final Configuration oldConfig = new Configuration(); + final Configuration newConfig = new Configuration(); + + oldConfig.screenWidthDp = 480; + // Test that value within bucket filters out screen size config + newConfig.screenWidthDp = 520; + assertEquals(0, SizeConfigurationBuckets.filterDiff(CONFIG_SCREEN_SIZE, oldConfig, + newConfig, sizeBuckets)); + // Test that value outside bucket does not filter out screen size config + newConfig.screenWidthDp = 640; + assertEquals(CONFIG_SCREEN_SIZE, SizeConfigurationBuckets.filterDiff(CONFIG_SCREEN_SIZE, + oldConfig, newConfig, sizeBuckets)); + } + + /** + * Tests that vertical buckets are correctly checked in + * {@link SizeConfigurationBuckets#filterDiff()}. + */ + @Test + public void testVerticalSizeThresholds() { + final int[] verticalThresholds = new int[] { 360, 600 }; + final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets( + null, verticalThresholds /* vertical */, null /* smallest */, + null /* screenLayoutSize */, false /* screenLayoutLongSet */); + + final Configuration oldConfig = new Configuration(); + final Configuration newConfig = new Configuration(); + + oldConfig.screenHeightDp = 480; + // Test that value within bucket filters out screen size config + newConfig.screenHeightDp = 520; + assertEquals(0, SizeConfigurationBuckets.filterDiff(CONFIG_SCREEN_SIZE, oldConfig, + newConfig, sizeBuckets)); + // Test that value outside bucket does not filter out screen size config + newConfig.screenHeightDp = 640; + assertEquals(CONFIG_SCREEN_SIZE, SizeConfigurationBuckets.filterDiff(CONFIG_SCREEN_SIZE, + oldConfig, newConfig, sizeBuckets)); + } + + /** + * Tests that smallest width buckets are correctly checked in + * {@link SizeConfigurationBuckets#filterDiff()}. + */ + @Test + public void testSmallestWidthSizeThresholds() { + final int[] smallestWidthThresholds = new int[] { 360, 600 }; + final SizeConfigurationBuckets sizeBuckets = new SizeConfigurationBuckets( + null, null /* vertical */, smallestWidthThresholds /* smallest */, + null /* screenLayoutSize */, false /* screenLayoutLongSet */); + + final Configuration oldConfig = new Configuration(); + final Configuration newConfig = new Configuration(); + + oldConfig.smallestScreenWidthDp = 480; + // Test that value within bucket filters out smallest screen size config + newConfig.smallestScreenWidthDp = 520; + assertEquals(0, SizeConfigurationBuckets.filterDiff(CONFIG_SMALLEST_SCREEN_SIZE, oldConfig, + newConfig, sizeBuckets)); + // Test that value outside bucket does not filter out smallest screen size config + newConfig.smallestScreenWidthDp = 640; + assertEquals(CONFIG_SMALLEST_SCREEN_SIZE, SizeConfigurationBuckets.filterDiff( + CONFIG_SMALLEST_SCREEN_SIZE, oldConfig, newConfig, sizeBuckets)); + } + + private void checkCrossesSizeThreshold(int[] thresholds, int oldValue, int newValue, + boolean expected) { + final String errorString = String.format( + "thresholds=%s, oldValue=%d, newValue=%d, expected=%b", Arrays.toString(thresholds), + oldValue, newValue, expected); + final boolean actual = SizeConfigurationBuckets.crossesSizeThreshold(thresholds, oldValue, + newValue); + assertEquals(errorString, expected, actual); + } +}