1 /* 2 * Copyright (C) 2021 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 17 package android.window; 18 19 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT; 20 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; 21 import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.content.res.Configuration; 26 import android.os.Parcelable; 27 import android.util.SparseIntArray; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.internal.util.DataClass; 31 32 import java.util.Arrays; 33 34 /** 35 * Contains size-configuration buckets used to prevent excessive configuration changes during 36 * resize. 37 * 38 * These configurations are collected from application's resources based on size-sensitive 39 * qualifiers. For example, layout-w800dp will be added to mHorizontalSizeConfigurations as 800 40 * and drawable-sw400dp will be added to both as 400. 41 * 42 * @hide 43 */ 44 @DataClass(genAidl = true) 45 public final class SizeConfigurationBuckets implements Parcelable { 46 47 /** Horizontal (screenWidthDp) buckets */ 48 @Nullable 49 private final int[] mHorizontal; 50 51 /** Vertical (screenHeightDp) buckets */ 52 @Nullable 53 private final int[] mVertical; 54 55 /** Smallest (smallestScreenWidthDp) buckets */ 56 @Nullable 57 private final int[] mSmallest; 58 59 /** Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets */ 60 @Nullable 61 private final int[] mScreenLayoutSize; 62 63 /** 64 * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a 65 * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and 66 * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change. 67 */ 68 private final boolean mScreenLayoutLongSet; 69 SizeConfigurationBuckets(Configuration[] sizeConfigurations)70 public SizeConfigurationBuckets(Configuration[] sizeConfigurations) { 71 SparseIntArray horizontal = new SparseIntArray(); 72 SparseIntArray vertical = new SparseIntArray(); 73 SparseIntArray smallest = new SparseIntArray(); 74 SparseIntArray screenLayoutSize = new SparseIntArray(); 75 int curScreenLayoutSize; 76 boolean screenLayoutLongSet = false; 77 for (int i = sizeConfigurations.length - 1; i >= 0; i--) { 78 Configuration config = sizeConfigurations[i]; 79 if (config.screenHeightDp != Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { 80 vertical.put(config.screenHeightDp, 0); 81 } 82 if (config.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED) { 83 horizontal.put(config.screenWidthDp, 0); 84 } 85 if (config.smallestScreenWidthDp != Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) { 86 smallest.put(config.smallestScreenWidthDp, 0); 87 } 88 if ((curScreenLayoutSize = config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) 89 != Configuration.SCREENLAYOUT_SIZE_UNDEFINED) { 90 screenLayoutSize.put(curScreenLayoutSize, 0); 91 } 92 if (!screenLayoutLongSet && (config.screenLayout & Configuration.SCREENLAYOUT_LONG_MASK) 93 != Configuration.SCREENLAYOUT_LONG_UNDEFINED) { 94 screenLayoutLongSet = true; 95 } 96 } 97 mHorizontal = horizontal.copyKeys(); 98 mVertical = vertical.copyKeys(); 99 mSmallest = smallest.copyKeys(); 100 mScreenLayoutSize = screenLayoutSize.copyKeys(); 101 mScreenLayoutLongSet = screenLayoutLongSet; 102 } 103 104 /** 105 * Get the changes between two configurations but don't count changes in sizes if they don't 106 * cross boundaries that are important to the app. 107 */ filterDiff(int diff, @NonNull Configuration oldConfig, @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets)108 public static int filterDiff(int diff, @NonNull Configuration oldConfig, 109 @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) { 110 if (buckets == null) { 111 return diff; 112 } 113 114 final boolean nonSizeLayoutFieldsUnchanged = 115 areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout); 116 if ((diff & CONFIG_SCREEN_SIZE) != 0) { 117 final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp, 118 newConfig.screenWidthDp) 119 || buckets.crossesVerticalSizeThreshold(oldConfig.screenHeightDp, 120 newConfig.screenHeightDp); 121 if (!crosses) { 122 diff &= ~CONFIG_SCREEN_SIZE; 123 } 124 } 125 if ((diff & CONFIG_SMALLEST_SCREEN_SIZE) != 0) { 126 final int oldSmallest = oldConfig.smallestScreenWidthDp; 127 final int newSmallest = newConfig.smallestScreenWidthDp; 128 if (!buckets.crossesSmallestSizeThreshold(oldSmallest, newSmallest)) { 129 diff &= ~CONFIG_SMALLEST_SCREEN_SIZE; 130 } 131 } 132 if ((diff & CONFIG_SCREEN_LAYOUT) != 0 && nonSizeLayoutFieldsUnchanged) { 133 if (!buckets.crossesScreenLayoutSizeThreshold(oldConfig, newConfig) 134 && !buckets.crossesScreenLayoutLongThreshold(oldConfig.screenLayout, 135 newConfig.screenLayout)) { 136 diff &= ~CONFIG_SCREEN_LAYOUT; 137 } 138 } 139 return diff; 140 } 141 crossesHorizontalSizeThreshold(int firstDp, int secondDp)142 private boolean crossesHorizontalSizeThreshold(int firstDp, int secondDp) { 143 return crossesSizeThreshold(mHorizontal, firstDp, secondDp); 144 } 145 crossesVerticalSizeThreshold(int firstDp, int secondDp)146 private boolean crossesVerticalSizeThreshold(int firstDp, int secondDp) { 147 return crossesSizeThreshold(mVertical, firstDp, secondDp); 148 } 149 crossesSmallestSizeThreshold(int firstDp, int secondDp)150 private boolean crossesSmallestSizeThreshold(int firstDp, int secondDp) { 151 return crossesSizeThreshold(mSmallest, firstDp, secondDp); 152 } 153 154 /** 155 * Returns whether a screen layout size threshold has been crossed. 156 */ 157 @VisibleForTesting crossesScreenLayoutSizeThreshold(@onNull Configuration firstConfig, @NonNull Configuration secondConfig)158 public boolean crossesScreenLayoutSizeThreshold(@NonNull Configuration firstConfig, 159 @NonNull Configuration secondConfig) { 160 // If both the old and new screen layout are equal (both can be undefined), then no 161 // threshold is crossed. 162 if ((firstConfig.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) 163 == (secondConfig.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)) { 164 return false; 165 } 166 // Any time the new layout size is smaller than the old layout size, the activity has 167 // crossed a size threshold because layout size represents the smallest possible size the 168 // activity can occupy. 169 if (!secondConfig.isLayoutSizeAtLeast(firstConfig.screenLayout 170 & Configuration.SCREENLAYOUT_SIZE_MASK)) { 171 return true; 172 } 173 // If the new layout size is at least as large as the old layout size, then check if the new 174 // layout size has crossed a threshold. 175 if (mScreenLayoutSize != null) { 176 for (int screenLayoutSize : mScreenLayoutSize) { 177 if (firstConfig.isLayoutSizeAtLeast(screenLayoutSize) 178 != secondConfig.isLayoutSizeAtLeast(screenLayoutSize)) { 179 return true; 180 } 181 } 182 } 183 return false; 184 } 185 crossesScreenLayoutLongThreshold(int firstScreenLayout, int secondScreenLayout)186 private boolean crossesScreenLayoutLongThreshold(int firstScreenLayout, 187 int secondScreenLayout) { 188 final int firstScreenLayoutLongValue = firstScreenLayout 189 & Configuration.SCREENLAYOUT_LONG_MASK; 190 final int secondScreenLayoutLongValue = secondScreenLayout 191 & Configuration.SCREENLAYOUT_LONG_MASK; 192 return mScreenLayoutLongSet && firstScreenLayoutLongValue != secondScreenLayoutLongValue; 193 } 194 195 /** 196 * Returns whether non-size related screen layout attributes have changed. If true, then 197 * {@link ActivityInfo#CONFIG_SCREEN_LAYOUT} should not be filtered out in 198 * {@link SizeConfigurationBuckets#filterDiff()} because the non-size related attributes 199 * do not have a bucket range like the size-related attributes of screen layout. 200 */ 201 @VisibleForTesting areNonSizeLayoutFieldsUnchanged(int oldScreenLayout, int newScreenLayout)202 public static boolean areNonSizeLayoutFieldsUnchanged(int oldScreenLayout, 203 int newScreenLayout) { 204 final int nonSizeRelatedFields = Configuration.SCREENLAYOUT_LAYOUTDIR_MASK 205 | Configuration.SCREENLAYOUT_ROUND_MASK | Configuration.SCREENLAYOUT_COMPAT_NEEDED; 206 return (oldScreenLayout & nonSizeRelatedFields) == (newScreenLayout & nonSizeRelatedFields); 207 } 208 209 /** 210 * The purpose of this method is to decide whether the activity needs to be relaunched upon 211 * changing its size. In most cases the activities don't need to be relaunched, if the resize 212 * is small, all the activity content has to do is relayout itself within new bounds. There are 213 * cases however, where the activity's content would be completely changed in the new size and 214 * the full relaunch is required. 215 * 216 * The activity will report to us vertical and horizontal thresholds after which a relaunch is 217 * required. These thresholds are collected from the application resource qualifiers. For 218 * example, if application has layout-w600dp resource directory, then it needs a relaunch when 219 * we resize from width of 650dp to 550dp, as it crosses the 600dp threshold. However, if 220 * it resizes width from 620dp to 700dp, it won't be relaunched as it stays on the same side 221 * of the threshold. 222 */ 223 @VisibleForTesting crossesSizeThreshold(int[] thresholds, int firstDp, int secondDp)224 public static boolean crossesSizeThreshold(int[] thresholds, int firstDp, 225 int secondDp) { 226 if (thresholds == null) { 227 return false; 228 } 229 for (int i = thresholds.length - 1; i >= 0; i--) { 230 final int threshold = thresholds[i]; 231 if ((firstDp < threshold && secondDp >= threshold) 232 || (firstDp >= threshold && secondDp < threshold)) { 233 return true; 234 } 235 } 236 return false; 237 } 238 239 @Override toString()240 public String toString() { 241 return Arrays.toString(mHorizontal) + " " + Arrays.toString(mVertical) + " " 242 + Arrays.toString(mSmallest) + " " + Arrays.toString(mScreenLayoutSize) + " " 243 + mScreenLayoutLongSet; 244 } 245 246 247 248 // Code below generated by codegen v1.0.23. 249 // 250 // DO NOT MODIFY! 251 // CHECKSTYLE:OFF Generated code 252 // 253 // To regenerate run: 254 // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/window/SizeConfigurationBuckets.java 255 // 256 // To exclude the generated code from IntelliJ auto-formatting enable (one-time): 257 // Settings > Editor > Code Style > Formatter Control 258 //@formatter:off 259 260 261 /** 262 * Creates a new SizeConfigurationBuckets. 263 * 264 * @param horizontal 265 * Horizontal (screenWidthDp) buckets 266 * @param vertical 267 * Vertical (screenHeightDp) buckets 268 * @param smallest 269 * Smallest (smallestScreenWidthDp) buckets 270 * @param screenLayoutSize 271 * Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets 272 * @param screenLayoutLongSet 273 * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a 274 * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and 275 * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change. 276 */ 277 @DataClass.Generated.Member SizeConfigurationBuckets( @ullable int[] horizontal, @Nullable int[] vertical, @Nullable int[] smallest, @Nullable int[] screenLayoutSize, boolean screenLayoutLongSet)278 public SizeConfigurationBuckets( 279 @Nullable int[] horizontal, 280 @Nullable int[] vertical, 281 @Nullable int[] smallest, 282 @Nullable int[] screenLayoutSize, 283 boolean screenLayoutLongSet) { 284 this.mHorizontal = horizontal; 285 this.mVertical = vertical; 286 this.mSmallest = smallest; 287 this.mScreenLayoutSize = screenLayoutSize; 288 this.mScreenLayoutLongSet = screenLayoutLongSet; 289 290 // onConstructed(); // You can define this method to get a callback 291 } 292 293 /** 294 * Horizontal (screenWidthDp) buckets 295 */ 296 @DataClass.Generated.Member getHorizontal()297 public @Nullable int[] getHorizontal() { 298 return mHorizontal; 299 } 300 301 /** 302 * Vertical (screenHeightDp) buckets 303 */ 304 @DataClass.Generated.Member getVertical()305 public @Nullable int[] getVertical() { 306 return mVertical; 307 } 308 309 /** 310 * Smallest (smallestScreenWidthDp) buckets 311 */ 312 @DataClass.Generated.Member getSmallest()313 public @Nullable int[] getSmallest() { 314 return mSmallest; 315 } 316 317 /** 318 * Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets 319 */ 320 @DataClass.Generated.Member getScreenLayoutSize()321 public @Nullable int[] getScreenLayoutSize() { 322 return mScreenLayoutSize; 323 } 324 325 /** 326 * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a 327 * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and 328 * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change. 329 */ 330 @DataClass.Generated.Member isScreenLayoutLongSet()331 public boolean isScreenLayoutLongSet() { 332 return mScreenLayoutLongSet; 333 } 334 335 @Override 336 @DataClass.Generated.Member writeToParcel(@onNull android.os.Parcel dest, int flags)337 public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { 338 // You can override field parcelling by defining methods like: 339 // void parcelFieldName(Parcel dest, int flags) { ... } 340 341 byte flg = 0; 342 if (mScreenLayoutLongSet) flg |= 0x10; 343 if (mHorizontal != null) flg |= 0x1; 344 if (mVertical != null) flg |= 0x2; 345 if (mSmallest != null) flg |= 0x4; 346 if (mScreenLayoutSize != null) flg |= 0x8; 347 dest.writeByte(flg); 348 if (mHorizontal != null) dest.writeIntArray(mHorizontal); 349 if (mVertical != null) dest.writeIntArray(mVertical); 350 if (mSmallest != null) dest.writeIntArray(mSmallest); 351 if (mScreenLayoutSize != null) dest.writeIntArray(mScreenLayoutSize); 352 } 353 354 @Override 355 @DataClass.Generated.Member describeContents()356 public int describeContents() { return 0; } 357 358 /** @hide */ 359 @SuppressWarnings({"unchecked", "RedundantCast"}) 360 @DataClass.Generated.Member SizeConfigurationBuckets(@onNull android.os.Parcel in)361 /* package-private */ SizeConfigurationBuckets(@NonNull android.os.Parcel in) { 362 // You can override field unparcelling by defining methods like: 363 // static FieldType unparcelFieldName(Parcel in) { ... } 364 365 byte flg = in.readByte(); 366 boolean screenLayoutLongSet = (flg & 0x10) != 0; 367 int[] horizontal = (flg & 0x1) == 0 ? null : in.createIntArray(); 368 int[] vertical = (flg & 0x2) == 0 ? null : in.createIntArray(); 369 int[] smallest = (flg & 0x4) == 0 ? null : in.createIntArray(); 370 int[] screenLayoutSize = (flg & 0x8) == 0 ? null : in.createIntArray(); 371 372 this.mHorizontal = horizontal; 373 this.mVertical = vertical; 374 this.mSmallest = smallest; 375 this.mScreenLayoutSize = screenLayoutSize; 376 this.mScreenLayoutLongSet = screenLayoutLongSet; 377 378 // onConstructed(); // You can define this method to get a callback 379 } 380 381 @DataClass.Generated.Member 382 public static final @NonNull Parcelable.Creator<SizeConfigurationBuckets> CREATOR 383 = new Parcelable.Creator<SizeConfigurationBuckets>() { 384 @Override 385 public SizeConfigurationBuckets[] newArray(int size) { 386 return new SizeConfigurationBuckets[size]; 387 } 388 389 @Override 390 public SizeConfigurationBuckets createFromParcel(@NonNull android.os.Parcel in) { 391 return new SizeConfigurationBuckets(in); 392 } 393 }; 394 395 @DataClass.Generated( 396 time = 1628273704583L, 397 codegenVersion = "1.0.23", 398 sourceFile = "frameworks/base/core/java/android/window/SizeConfigurationBuckets.java", 399 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)") 400 @Deprecated __metadata()401 private void __metadata() {} 402 403 404 //@formatter:on 405 // End of generated code 406 407 } 408