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 * 108 * This is a static helper to deal with null `buckets`. When no buckets have been specified, 109 * this actually filters out all 3 size-configs. This is legacy behavior. 110 */ filterDiff(int diff, @NonNull Configuration oldConfig, @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets)111 public static int filterDiff(int diff, @NonNull Configuration oldConfig, 112 @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) { 113 final boolean nonSizeLayoutFieldsUnchanged = 114 areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout); 115 if (buckets == null) { 116 // Only unflip CONFIG_SCREEN_LAYOUT if non-size-related attributes of screen layout do 117 // not change. 118 if (nonSizeLayoutFieldsUnchanged) { 119 return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE 120 | CONFIG_SCREEN_LAYOUT); 121 } else { 122 return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE); 123 } 124 } 125 if ((diff & CONFIG_SCREEN_SIZE) != 0) { 126 final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp, 127 newConfig.screenWidthDp) 128 || buckets.crossesVerticalSizeThreshold(oldConfig.screenHeightDp, 129 newConfig.screenHeightDp); 130 if (!crosses) { 131 diff &= ~CONFIG_SCREEN_SIZE; 132 } 133 } 134 if ((diff & CONFIG_SMALLEST_SCREEN_SIZE) != 0) { 135 final int oldSmallest = oldConfig.smallestScreenWidthDp; 136 final int newSmallest = newConfig.smallestScreenWidthDp; 137 if (!buckets.crossesSmallestSizeThreshold(oldSmallest, newSmallest)) { 138 diff &= ~CONFIG_SMALLEST_SCREEN_SIZE; 139 } 140 } 141 if ((diff & CONFIG_SCREEN_LAYOUT) != 0 && nonSizeLayoutFieldsUnchanged) { 142 if (!buckets.crossesScreenLayoutSizeThreshold(oldConfig, newConfig) 143 && !buckets.crossesScreenLayoutLongThreshold(oldConfig.screenLayout, 144 newConfig.screenLayout)) { 145 diff &= ~CONFIG_SCREEN_LAYOUT; 146 } 147 } 148 return diff; 149 } 150 crossesHorizontalSizeThreshold(int firstDp, int secondDp)151 private boolean crossesHorizontalSizeThreshold(int firstDp, int secondDp) { 152 return crossesSizeThreshold(mHorizontal, firstDp, secondDp); 153 } 154 crossesVerticalSizeThreshold(int firstDp, int secondDp)155 private boolean crossesVerticalSizeThreshold(int firstDp, int secondDp) { 156 return crossesSizeThreshold(mVertical, firstDp, secondDp); 157 } 158 crossesSmallestSizeThreshold(int firstDp, int secondDp)159 private boolean crossesSmallestSizeThreshold(int firstDp, int secondDp) { 160 return crossesSizeThreshold(mSmallest, firstDp, secondDp); 161 } 162 163 /** 164 * Returns whether a screen layout size threshold has been crossed. 165 */ 166 @VisibleForTesting crossesScreenLayoutSizeThreshold(@onNull Configuration firstConfig, @NonNull Configuration secondConfig)167 public boolean crossesScreenLayoutSizeThreshold(@NonNull Configuration firstConfig, 168 @NonNull Configuration secondConfig) { 169 // If both the old and new screen layout are equal (both can be undefined), then no 170 // threshold is crossed. 171 if ((firstConfig.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) 172 == (secondConfig.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)) { 173 return false; 174 } 175 // Any time the new layout size is smaller than the old layout size, the activity has 176 // crossed a size threshold because layout size represents the smallest possible size the 177 // activity can occupy. 178 if (!secondConfig.isLayoutSizeAtLeast(firstConfig.screenLayout 179 & Configuration.SCREENLAYOUT_SIZE_MASK)) { 180 return true; 181 } 182 // If the new layout size is at least as large as the old layout size, then check if the new 183 // layout size has crossed a threshold. 184 if (mScreenLayoutSize != null) { 185 for (int screenLayoutSize : mScreenLayoutSize) { 186 if (firstConfig.isLayoutSizeAtLeast(screenLayoutSize) 187 != secondConfig.isLayoutSizeAtLeast(screenLayoutSize)) { 188 return true; 189 } 190 } 191 } 192 return false; 193 } 194 crossesScreenLayoutLongThreshold(int firstScreenLayout, int secondScreenLayout)195 private boolean crossesScreenLayoutLongThreshold(int firstScreenLayout, 196 int secondScreenLayout) { 197 final int firstScreenLayoutLongValue = firstScreenLayout 198 & Configuration.SCREENLAYOUT_LONG_MASK; 199 final int secondScreenLayoutLongValue = secondScreenLayout 200 & Configuration.SCREENLAYOUT_LONG_MASK; 201 return mScreenLayoutLongSet && firstScreenLayoutLongValue != secondScreenLayoutLongValue; 202 } 203 204 /** 205 * Returns whether non-size related screen layout attributes have changed. If true, then 206 * {@link ActivityInfo#CONFIG_SCREEN_LAYOUT} should not be filtered out in 207 * {@link SizeConfigurationBuckets#filterDiff()} because the non-size related attributes 208 * do not have a bucket range like the size-related attributes of screen layout. 209 */ 210 @VisibleForTesting areNonSizeLayoutFieldsUnchanged(int oldScreenLayout, int newScreenLayout)211 public static boolean areNonSizeLayoutFieldsUnchanged(int oldScreenLayout, 212 int newScreenLayout) { 213 final int nonSizeRelatedFields = Configuration.SCREENLAYOUT_LAYOUTDIR_MASK 214 | Configuration.SCREENLAYOUT_ROUND_MASK | Configuration.SCREENLAYOUT_COMPAT_NEEDED; 215 return (oldScreenLayout & nonSizeRelatedFields) == (newScreenLayout & nonSizeRelatedFields); 216 } 217 218 /** 219 * The purpose of this method is to decide whether the activity needs to be relaunched upon 220 * changing its size. In most cases the activities don't need to be relaunched, if the resize 221 * is small, all the activity content has to do is relayout itself within new bounds. There are 222 * cases however, where the activity's content would be completely changed in the new size and 223 * the full relaunch is required. 224 * 225 * The activity will report to us vertical and horizontal thresholds after which a relaunch is 226 * required. These thresholds are collected from the application resource qualifiers. For 227 * example, if application has layout-w600dp resource directory, then it needs a relaunch when 228 * we resize from width of 650dp to 550dp, as it crosses the 600dp threshold. However, if 229 * it resizes width from 620dp to 700dp, it won't be relaunched as it stays on the same side 230 * of the threshold. 231 */ 232 @VisibleForTesting crossesSizeThreshold(int[] thresholds, int firstDp, int secondDp)233 public static boolean crossesSizeThreshold(int[] thresholds, int firstDp, 234 int secondDp) { 235 if (thresholds == null) { 236 return false; 237 } 238 for (int i = thresholds.length - 1; i >= 0; i--) { 239 final int threshold = thresholds[i]; 240 if ((firstDp < threshold && secondDp >= threshold) 241 || (firstDp >= threshold && secondDp < threshold)) { 242 return true; 243 } 244 } 245 return false; 246 } 247 248 @Override toString()249 public String toString() { 250 return Arrays.toString(mHorizontal) + " " + Arrays.toString(mVertical) + " " 251 + Arrays.toString(mSmallest) + " " + Arrays.toString(mScreenLayoutSize) + " " 252 + mScreenLayoutLongSet; 253 } 254 255 256 257 // Code below generated by codegen v1.0.23. 258 // 259 // DO NOT MODIFY! 260 // CHECKSTYLE:OFF Generated code 261 // 262 // To regenerate run: 263 // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/window/SizeConfigurationBuckets.java 264 // 265 // To exclude the generated code from IntelliJ auto-formatting enable (one-time): 266 // Settings > Editor > Code Style > Formatter Control 267 //@formatter:off 268 269 270 /** 271 * Creates a new SizeConfigurationBuckets. 272 * 273 * @param horizontal 274 * Horizontal (screenWidthDp) buckets 275 * @param vertical 276 * Vertical (screenHeightDp) buckets 277 * @param smallest 278 * Smallest (smallestScreenWidthDp) buckets 279 * @param screenLayoutSize 280 * Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets 281 * @param screenLayoutLongSet 282 * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a 283 * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and 284 * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change. 285 */ 286 @DataClass.Generated.Member SizeConfigurationBuckets( @ullable int[] horizontal, @Nullable int[] vertical, @Nullable int[] smallest, @Nullable int[] screenLayoutSize, boolean screenLayoutLongSet)287 public SizeConfigurationBuckets( 288 @Nullable int[] horizontal, 289 @Nullable int[] vertical, 290 @Nullable int[] smallest, 291 @Nullable int[] screenLayoutSize, 292 boolean screenLayoutLongSet) { 293 this.mHorizontal = horizontal; 294 this.mVertical = vertical; 295 this.mSmallest = smallest; 296 this.mScreenLayoutSize = screenLayoutSize; 297 this.mScreenLayoutLongSet = screenLayoutLongSet; 298 299 // onConstructed(); // You can define this method to get a callback 300 } 301 302 /** 303 * Horizontal (screenWidthDp) buckets 304 */ 305 @DataClass.Generated.Member getHorizontal()306 public @Nullable int[] getHorizontal() { 307 return mHorizontal; 308 } 309 310 /** 311 * Vertical (screenHeightDp) buckets 312 */ 313 @DataClass.Generated.Member getVertical()314 public @Nullable int[] getVertical() { 315 return mVertical; 316 } 317 318 /** 319 * Smallest (smallestScreenWidthDp) buckets 320 */ 321 @DataClass.Generated.Member getSmallest()322 public @Nullable int[] getSmallest() { 323 return mSmallest; 324 } 325 326 /** 327 * Screen Layout Size (screenLayout & SCREENLAYOUT_SIZE_MASK) buckets 328 */ 329 @DataClass.Generated.Member getScreenLayoutSize()330 public @Nullable int[] getScreenLayoutSize() { 331 return mScreenLayoutSize; 332 } 333 334 /** 335 * Screen Layout Long (screenLayout & SCREENLAYOUT_LONG_MASK) boolean. Only need to know if a 336 * value is set because only two possible buckets, SCREENLAYOUT_LONG_NO and 337 * SCREENLAYOUT_LONG_YES, so if either is set, then any change is a bucket change. 338 */ 339 @DataClass.Generated.Member isScreenLayoutLongSet()340 public boolean isScreenLayoutLongSet() { 341 return mScreenLayoutLongSet; 342 } 343 344 @Override 345 @DataClass.Generated.Member writeToParcel(@onNull android.os.Parcel dest, int flags)346 public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { 347 // You can override field parcelling by defining methods like: 348 // void parcelFieldName(Parcel dest, int flags) { ... } 349 350 byte flg = 0; 351 if (mScreenLayoutLongSet) flg |= 0x10; 352 if (mHorizontal != null) flg |= 0x1; 353 if (mVertical != null) flg |= 0x2; 354 if (mSmallest != null) flg |= 0x4; 355 if (mScreenLayoutSize != null) flg |= 0x8; 356 dest.writeByte(flg); 357 if (mHorizontal != null) dest.writeIntArray(mHorizontal); 358 if (mVertical != null) dest.writeIntArray(mVertical); 359 if (mSmallest != null) dest.writeIntArray(mSmallest); 360 if (mScreenLayoutSize != null) dest.writeIntArray(mScreenLayoutSize); 361 } 362 363 @Override 364 @DataClass.Generated.Member describeContents()365 public int describeContents() { return 0; } 366 367 /** @hide */ 368 @SuppressWarnings({"unchecked", "RedundantCast"}) 369 @DataClass.Generated.Member SizeConfigurationBuckets(@onNull android.os.Parcel in)370 /* package-private */ SizeConfigurationBuckets(@NonNull android.os.Parcel in) { 371 // You can override field unparcelling by defining methods like: 372 // static FieldType unparcelFieldName(Parcel in) { ... } 373 374 byte flg = in.readByte(); 375 boolean screenLayoutLongSet = (flg & 0x10) != 0; 376 int[] horizontal = (flg & 0x1) == 0 ? null : in.createIntArray(); 377 int[] vertical = (flg & 0x2) == 0 ? null : in.createIntArray(); 378 int[] smallest = (flg & 0x4) == 0 ? null : in.createIntArray(); 379 int[] screenLayoutSize = (flg & 0x8) == 0 ? null : in.createIntArray(); 380 381 this.mHorizontal = horizontal; 382 this.mVertical = vertical; 383 this.mSmallest = smallest; 384 this.mScreenLayoutSize = screenLayoutSize; 385 this.mScreenLayoutLongSet = screenLayoutLongSet; 386 387 // onConstructed(); // You can define this method to get a callback 388 } 389 390 @DataClass.Generated.Member 391 public static final @NonNull Parcelable.Creator<SizeConfigurationBuckets> CREATOR 392 = new Parcelable.Creator<SizeConfigurationBuckets>() { 393 @Override 394 public SizeConfigurationBuckets[] newArray(int size) { 395 return new SizeConfigurationBuckets[size]; 396 } 397 398 @Override 399 public SizeConfigurationBuckets createFromParcel(@NonNull android.os.Parcel in) { 400 return new SizeConfigurationBuckets(in); 401 } 402 }; 403 404 @DataClass.Generated( 405 time = 1628273704583L, 406 codegenVersion = "1.0.23", 407 sourceFile = "frameworks/base/core/java/android/window/SizeConfigurationBuckets.java", 408 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)") 409 @Deprecated __metadata()410 private void __metadata() {} 411 412 413 //@formatter:on 414 // End of generated code 415 416 } 417