1 /* 2 * Copyright (C) 2023 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.server.wm.jetpack.extensions.util; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertTrue; 23 import static org.junit.Assume.assumeFalse; 24 import static org.junit.Assume.assumeNotNull; 25 import static org.junit.Assume.assumeTrue; 26 27 import android.app.Activity; 28 import android.content.Context; 29 import android.graphics.Rect; 30 import android.util.Log; 31 import android.view.WindowManager; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.annotation.UiContext; 36 import androidx.window.extensions.WindowExtensions; 37 import androidx.window.extensions.WindowExtensionsProvider; 38 import androidx.window.extensions.area.WindowAreaComponent; 39 import androidx.window.extensions.layout.DisplayFeature; 40 import androidx.window.extensions.layout.FoldingFeature; 41 import androidx.window.extensions.layout.WindowLayoutComponent; 42 import androidx.window.extensions.layout.WindowLayoutInfo; 43 44 import com.android.window.flags.Flags; 45 46 import java.util.List; 47 import java.util.stream.Collectors; 48 49 /** 50 * Utility class for extensions tests, providing methods for checking if a device supports 51 * extensions, retrieving and validating the extension version, and getting the instance of 52 * {@link WindowExtensions}. 53 */ 54 public class ExtensionsUtil { 55 56 private static final String EXTENSION_TAG = "Extension"; 57 58 public static final int EXTENSION_VERSION_DISABLED = 0; 59 60 /** 61 * See <a href="https://source.android.com/docs/core/display/windowmanager-extensions#extensions_versions_and_updates"> 62 * Extensions versions</a>. 63 */ 64 public static final int EXTENSION_VERSION_CURRENT_PLATFORM_V7 = 7; 65 public static final int EXTENSION_VERSION_CURRENT_PLATFORM_V8 = 8; 66 public static final int EXTENSION_VERSION_CURRENT_PLATFORM_V9 = 9; 67 68 /** 69 * Returns the current version of {@link WindowExtensions} if present on the device. 70 */ getExtensionVersion()71 public static int getExtensionVersion() { 72 try { 73 WindowExtensions extensions = getWindowExtensions(); 74 if (extensions != null) { 75 return extensions.getVendorApiLevel(); 76 } 77 } catch (NoClassDefFoundError e) { 78 Log.d(EXTENSION_TAG, "Extension version not found"); 79 } catch (UnsupportedOperationException e) { 80 Log.d(EXTENSION_TAG, "Stub Extension"); 81 } 82 return EXTENSION_VERSION_DISABLED; 83 } 84 85 /** 86 * Returns {@code true} if the version reported on the device is at least the version provided. 87 * This is used in CTS tests to try to add coverage without strict enforcement. We can not apply 88 * strict enforcement between dessert releases. 89 * @param targetVersion minimum version to be checked. 90 * @return true if the version on the device is at least the target version inclusively. 91 */ isExtensionVersionAtLeast(int targetVersion)92 public static boolean isExtensionVersionAtLeast(int targetVersion) { 93 final int version = getExtensionVersion(); 94 return version >= targetVersion; 95 } 96 97 /** 98 * Returns {@code true} if the version reported on the device is greater than or equal to the 99 * corresponding platform version. 100 */ isExtensionVersionLatest()101 public static boolean isExtensionVersionLatest() { 102 if (Flags.wlinfoOncreate()) { 103 return isExtensionVersionAtLeast(EXTENSION_VERSION_CURRENT_PLATFORM_V9); 104 } else if (Flags.aeBackStackRestore()) { 105 return isExtensionVersionAtLeast(EXTENSION_VERSION_CURRENT_PLATFORM_V8); 106 } else { 107 return isExtensionVersionAtLeast(EXTENSION_VERSION_CURRENT_PLATFORM_V7); 108 } 109 } 110 111 /** 112 * If called on a device with the vendor api level less than the bound then the test will be 113 * ignored. 114 * @param vendorApiLevel minimum {@link WindowExtensions#getVendorApiLevel()} for a test to 115 * succeed 116 */ assumeVendorApiLevelAtLeast(int vendorApiLevel)117 public static void assumeVendorApiLevelAtLeast(int vendorApiLevel) { 118 final int version = getExtensionVersion(); 119 assumeTrue( 120 "Needs vendorApiLevel " + vendorApiLevel + " but has " + version, 121 version >= vendorApiLevel 122 ); 123 } 124 125 /** 126 * Returns {@code true} if the extensions version is greater than 0. 127 */ isExtensionVersionValid()128 public static boolean isExtensionVersionValid() { 129 final int version = getExtensionVersion(); 130 // Check that the extension version on the device is at least the minimum valid version. 131 return version > EXTENSION_VERSION_DISABLED; 132 } 133 134 /** 135 * Returns the {@link WindowExtensions} if it is present on the device, {@code null} otherwise. 136 */ 137 @Nullable getWindowExtensions()138 public static WindowExtensions getWindowExtensions() { 139 try { 140 return WindowExtensionsProvider.getWindowExtensions(); 141 } catch (NoClassDefFoundError e) { 142 Log.d(EXTENSION_TAG, "Extension implementation not found"); 143 } catch (UnsupportedOperationException e) { 144 Log.d(EXTENSION_TAG, "Stub Extension"); 145 } 146 return null; 147 } 148 149 /** 150 * Assumes that extensions is present on the device. 151 */ assumeExtensionSupportedDevice()152 public static void assumeExtensionSupportedDevice() { 153 assumeNotNull("Device does not contain extensions library", getWindowExtensions()); 154 assumeTrue("Device doesn't config to support extensions", 155 WindowManager.hasWindowExtensionsEnabled()); 156 } 157 158 /** 159 * Returns the {@link WindowLayoutComponent} if it is present on the device, {@code null} 160 * otherwise. 161 */ 162 @Nullable getExtensionWindowLayoutComponent()163 public static WindowLayoutComponent getExtensionWindowLayoutComponent() { 164 WindowExtensions extension = getWindowExtensions(); 165 if (extension == null) { 166 return null; 167 } 168 return extension.getWindowLayoutComponent(); 169 } 170 171 /** 172 * Publishes a WindowLayoutInfo update to a test consumer. Both type WindowContext and Activity 173 * can be listeners. This method should be called at most once for each given Context because 174 * {@link WindowLayoutComponent#addWindowLayoutInfoListener} implementation assumes a 1-1 175 * mapping between the context and consumer. 176 */ 177 @Nullable getExtensionWindowLayoutInfo(@iContext Context context)178 public static WindowLayoutInfo getExtensionWindowLayoutInfo(@UiContext Context context) 179 throws InterruptedException { 180 WindowLayoutComponent windowLayoutComponent = getExtensionWindowLayoutComponent(); 181 if (windowLayoutComponent == null) { 182 return null; 183 } 184 TestValueCountConsumer<WindowLayoutInfo> windowLayoutInfoConsumer = 185 new TestValueCountConsumer<>(); 186 windowLayoutComponent.addWindowLayoutInfoListener(context, windowLayoutInfoConsumer); 187 WindowLayoutInfo info = windowLayoutInfoConsumer.waitAndGet(); 188 189 // The default implementation only allows a single listener per context. Since we are using 190 // a local windowLayoutInfoConsumer within this function, we must remember to clean up. 191 // Otherwise, subsequent calls to addWindowLayoutInfoListener with the same context will 192 // fail to have its callback registered. 193 windowLayoutComponent.removeWindowLayoutInfoListener(windowLayoutInfoConsumer); 194 return info; 195 } 196 197 /** 198 * Returns an int array containing the raw values of the currently visible fold types. 199 * @param activity An {@link Activity} that is visible and intersects the folds 200 * @return an int array containing the raw values for the current visible fold types. 201 * @throws InterruptedException when the async collection of the {@link WindowLayoutInfo} 202 * is interrupted. 203 */ 204 @NonNull getExtensionDisplayFeatureTypes(Activity activity)205 public static int[] getExtensionDisplayFeatureTypes(Activity activity) 206 throws InterruptedException { 207 WindowLayoutInfo windowLayoutInfo = getExtensionWindowLayoutInfo(activity); 208 if (windowLayoutInfo == null) { 209 return new int[0]; 210 } 211 List<DisplayFeature> displayFeatureList = windowLayoutInfo.getDisplayFeatures(); 212 return displayFeatureList 213 .stream() 214 .filter(d -> d instanceof FoldingFeature) 215 .map(d -> ((FoldingFeature) d).getType()) 216 .mapToInt(i -> i.intValue()) 217 .toArray(); 218 } 219 220 /** 221 * Returns whether the device reports at least one display feature. 222 */ assumeHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo)223 public static void assumeHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo) { 224 // If WindowLayoutComponent is implemented, then WindowLayoutInfo and the list of display 225 // features cannot be null. However the list can be empty if the device does not report 226 // any display features. 227 assertNotNull(windowLayoutInfo); 228 assertNotNull(windowLayoutInfo.getDisplayFeatures()); 229 assumeFalse(windowLayoutInfo.getDisplayFeatures().isEmpty()); 230 } 231 232 /** 233 * Asserts that the {@link WindowLayoutInfo} is not empty. 234 */ assertHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo)235 public static void assertHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo) { 236 // If WindowLayoutComponent is implemented, then WindowLayoutInfo and the list of display 237 // features cannot be null. However the list can be empty if the device does not report 238 // any display features. 239 assertNotNull(windowLayoutInfo); 240 assertNotNull(windowLayoutInfo.getDisplayFeatures()); 241 assertFalse(windowLayoutInfo.getDisplayFeatures().isEmpty()); 242 } 243 244 /** 245 * Checks that display features are consistent across portrait and landscape orientations. 246 * It is possible for the display features to be different between portrait and landscape 247 * orientations because only display features within the activity bounds are provided to the 248 * activity and the activity may be letterboxed if orientation requests are ignored. So, only 249 * check that display features that are within both portrait and landscape activity bounds 250 * are consistent. To be consistent, the feature bounds must be the same (potentially rotated if 251 * orientation requests are respected) and their type and state must be the same. 252 */ assertEqualWindowLayoutInfo( @onNull WindowLayoutInfo portraitWindowLayoutInfo, @NonNull WindowLayoutInfo landscapeWindowLayoutInfo, @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, boolean doesDisplayRotateForOrientation)253 public static void assertEqualWindowLayoutInfo( 254 @NonNull WindowLayoutInfo portraitWindowLayoutInfo, 255 @NonNull WindowLayoutInfo landscapeWindowLayoutInfo, 256 @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, 257 boolean doesDisplayRotateForOrientation) { 258 // Compute the portrait and landscape features that are within both the portrait and 259 // landscape activity bounds. 260 final List<DisplayFeature> portraitFeaturesWithinBoth = getMutualDisplayFeatures( 261 portraitWindowLayoutInfo, portraitBounds, landscapeBounds); 262 List<DisplayFeature> landscapeFeaturesWithinBoth = getMutualDisplayFeatures( 263 landscapeWindowLayoutInfo, landscapeBounds, portraitBounds); 264 assertEquals(portraitFeaturesWithinBoth.size(), landscapeFeaturesWithinBoth.size()); 265 final int nFeatures = portraitFeaturesWithinBoth.size(); 266 if (nFeatures == 0) { 267 return; 268 } 269 270 // If the display rotates to respect orientation, then to make the landscape display 271 // features comparable to the portrait display features rotate the landscape features. 272 if (doesDisplayRotateForOrientation) { 273 landscapeFeaturesWithinBoth = landscapeFeaturesWithinBoth 274 .stream() 275 .map(d -> { 276 if (!(d instanceof FoldingFeature)) { 277 return d; 278 } 279 final FoldingFeature f = (FoldingFeature) d; 280 final Rect oldBounds = d.getBounds(); 281 // Rotate the bounds by 90 degrees 282 final Rect newBounds = new Rect(oldBounds.top, oldBounds.left, 283 oldBounds.bottom, oldBounds.right); 284 return new FoldingFeature(newBounds, f.getType(), f.getState()); 285 }) 286 .collect(Collectors.toList()); 287 } 288 289 // Check that the list of features are the same 290 final boolean[] portraitFeatureMatched = new boolean[nFeatures]; 291 final boolean[] landscapeFeatureMatched = new boolean[nFeatures]; 292 for (int portraitIndex = 0; portraitIndex < nFeatures; portraitIndex++) { 293 if (portraitFeatureMatched[portraitIndex]) { 294 // A match has already been found for this portrait display feature 295 continue; 296 } 297 final DisplayFeature portraitDisplayFeature = portraitFeaturesWithinBoth 298 .get(portraitIndex); 299 for (int landscapeIndex = 0; landscapeIndex < nFeatures; landscapeIndex++) { 300 if (landscapeFeatureMatched[landscapeIndex]) { 301 // A match has already been found for this landscape display feature 302 continue; 303 } 304 final DisplayFeature landscapeDisplayFeature = landscapeFeaturesWithinBoth 305 .get(landscapeIndex); 306 // Only continue comparing if both display features are the same type of display 307 // feature (e.g. FoldingFeature) and they have the same bounds 308 if (!portraitDisplayFeature.getClass().equals(landscapeDisplayFeature.getClass()) 309 || !portraitDisplayFeature.getBounds().equals( 310 landscapeDisplayFeature.getBounds())) { 311 continue; 312 } 313 // If both are folding features, then only continue comparing if the type and state 314 // match 315 if (portraitDisplayFeature instanceof FoldingFeature) { 316 FoldingFeature portraitFoldingFeature = (FoldingFeature) portraitDisplayFeature; 317 FoldingFeature landscapeFoldingFeature = 318 (FoldingFeature) landscapeDisplayFeature; 319 if (portraitFoldingFeature.getType() != landscapeFoldingFeature.getType() 320 || portraitFoldingFeature.getState() 321 != landscapeFoldingFeature.getState()) { 322 continue; 323 } 324 } 325 // The display features match 326 portraitFeatureMatched[portraitIndex] = true; 327 landscapeFeatureMatched[landscapeIndex] = true; 328 } 329 } 330 331 // Check that a match was found for each display feature 332 for (int i = 0; i < nFeatures; i++) { 333 assertTrue(portraitFeatureMatched[i] && landscapeFeatureMatched[i]); 334 } 335 } 336 337 /** 338 * Returns the subset of {@code windowLayoutInfo} display features that are shared by the 339 * activity bounds in the current orientation and the activity bounds in the other orientation. 340 */ getMutualDisplayFeatures( @onNull WindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds, @NonNull Rect otherOrientationBounds)341 private static List<DisplayFeature> getMutualDisplayFeatures( 342 @NonNull WindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds, 343 @NonNull Rect otherOrientationBounds) { 344 return windowLayoutInfo 345 .getDisplayFeatures() 346 .stream() 347 .map(d -> { 348 if (!(d instanceof FoldingFeature)) { 349 return d; 350 } 351 // The display features are positioned relative to the activity bounds, so 352 // re-position them absolutely within the task. 353 final FoldingFeature f = (FoldingFeature) d; 354 final Rect r = f.getBounds(); 355 r.offset(currentOrientationBounds.left, currentOrientationBounds.top); 356 return new FoldingFeature(r, f.getType(), f.getState()); 357 }) 358 .filter(d -> otherOrientationBounds.contains(d.getBounds())) 359 .collect(Collectors.toList()); 360 } 361 362 /** 363 * Returns the {@link WindowAreaComponent} available in {@link WindowExtensions} if available. 364 * If the component is not available, returns null. 365 */ 366 @Nullable 367 public static WindowAreaComponent getExtensionWindowAreaComponent() { 368 final WindowExtensions extension = getWindowExtensions(); 369 return extension != null 370 ? extension.getWindowAreaComponent() 371 : null; 372 } 373 } 374