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 { 105 return isExtensionVersionAtLeast(EXTENSION_VERSION_CURRENT_PLATFORM_V8); 106 } 107 } 108 109 /** 110 * If called on a device with the vendor api level less than the bound then the test will be 111 * ignored. 112 * @param vendorApiLevel minimum {@link WindowExtensions#getVendorApiLevel()} for a test to 113 * succeed 114 */ assumeVendorApiLevelAtLeast(int vendorApiLevel)115 public static void assumeVendorApiLevelAtLeast(int vendorApiLevel) { 116 final int version = getExtensionVersion(); 117 assumeTrue( 118 "Needs vendorApiLevel " + vendorApiLevel + " but has " + version, 119 version >= vendorApiLevel 120 ); 121 } 122 123 /** 124 * Returns {@code true} if the extensions version is greater than 0. 125 */ isExtensionVersionValid()126 public static boolean isExtensionVersionValid() { 127 final int version = getExtensionVersion(); 128 // Check that the extension version on the device is at least the minimum valid version. 129 return version > EXTENSION_VERSION_DISABLED; 130 } 131 132 /** 133 * Returns the {@link WindowExtensions} if it is present on the device, {@code null} otherwise. 134 */ 135 @Nullable getWindowExtensions()136 public static WindowExtensions getWindowExtensions() { 137 try { 138 return WindowExtensionsProvider.getWindowExtensions(); 139 } catch (NoClassDefFoundError e) { 140 Log.d(EXTENSION_TAG, "Extension implementation not found"); 141 } catch (UnsupportedOperationException e) { 142 Log.d(EXTENSION_TAG, "Stub Extension"); 143 } 144 return null; 145 } 146 147 /** 148 * Assumes that extensions is present on the device. 149 */ assumeExtensionSupportedDevice()150 public static void assumeExtensionSupportedDevice() { 151 assumeNotNull("Device does not contain extensions library", getWindowExtensions()); 152 assumeTrue("Device doesn't config to support extensions", 153 WindowManager.hasWindowExtensionsEnabled()); 154 } 155 156 /** 157 * Returns the {@link WindowLayoutComponent} if it is present on the device, {@code null} 158 * otherwise. 159 */ 160 @Nullable getExtensionWindowLayoutComponent()161 public static WindowLayoutComponent getExtensionWindowLayoutComponent() { 162 WindowExtensions extension = getWindowExtensions(); 163 if (extension == null) { 164 return null; 165 } 166 return extension.getWindowLayoutComponent(); 167 } 168 169 /** 170 * Publishes a WindowLayoutInfo update to a test consumer. Both type WindowContext and Activity 171 * can be listeners. This method should be called at most once for each given Context because 172 * {@link WindowLayoutComponent#addWindowLayoutInfoListener} implementation assumes a 1-1 173 * mapping between the context and consumer. 174 */ 175 @Nullable getExtensionWindowLayoutInfo(@iContext Context context)176 public static WindowLayoutInfo getExtensionWindowLayoutInfo(@UiContext Context context) 177 throws InterruptedException { 178 WindowLayoutComponent windowLayoutComponent = getExtensionWindowLayoutComponent(); 179 if (windowLayoutComponent == null) { 180 return null; 181 } 182 TestValueCountConsumer<WindowLayoutInfo> windowLayoutInfoConsumer = 183 new TestValueCountConsumer<>(); 184 windowLayoutComponent.addWindowLayoutInfoListener(context, windowLayoutInfoConsumer); 185 WindowLayoutInfo info = windowLayoutInfoConsumer.waitAndGet(); 186 187 // The default implementation only allows a single listener per context. Since we are using 188 // a local windowLayoutInfoConsumer within this function, we must remember to clean up. 189 // Otherwise, subsequent calls to addWindowLayoutInfoListener with the same context will 190 // fail to have its callback registered. 191 windowLayoutComponent.removeWindowLayoutInfoListener(windowLayoutInfoConsumer); 192 return info; 193 } 194 195 /** 196 * Returns an int array containing the raw values of the currently visible fold types. 197 * @param activity An {@link Activity} that is visible and intersects the folds 198 * @return an int array containing the raw values for the current visible fold types. 199 * @throws InterruptedException when the async collection of the {@link WindowLayoutInfo} 200 * is interrupted. 201 */ 202 @NonNull getExtensionDisplayFeatureTypes(Activity activity)203 public static int[] getExtensionDisplayFeatureTypes(Activity activity) 204 throws InterruptedException { 205 WindowLayoutInfo windowLayoutInfo = getExtensionWindowLayoutInfo(activity); 206 if (windowLayoutInfo == null) { 207 return new int[0]; 208 } 209 List<DisplayFeature> displayFeatureList = windowLayoutInfo.getDisplayFeatures(); 210 return displayFeatureList 211 .stream() 212 .filter(d -> d instanceof FoldingFeature) 213 .map(d -> ((FoldingFeature) d).getType()) 214 .mapToInt(i -> i.intValue()) 215 .toArray(); 216 } 217 218 /** 219 * Returns whether the device reports at least one display feature. 220 */ assumeHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo)221 public static void assumeHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo) { 222 // If WindowLayoutComponent is implemented, then WindowLayoutInfo and the list of display 223 // features cannot be null. However the list can be empty if the device does not report 224 // any display features. 225 assertNotNull(windowLayoutInfo); 226 assertNotNull(windowLayoutInfo.getDisplayFeatures()); 227 assumeFalse(windowLayoutInfo.getDisplayFeatures().isEmpty()); 228 } 229 230 /** 231 * Asserts that the {@link WindowLayoutInfo} is not empty. 232 */ assertHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo)233 public static void assertHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo) { 234 // If WindowLayoutComponent is implemented, then WindowLayoutInfo and the list of display 235 // features cannot be null. However the list can be empty if the device does not report 236 // any display features. 237 assertNotNull(windowLayoutInfo); 238 assertNotNull(windowLayoutInfo.getDisplayFeatures()); 239 assertFalse(windowLayoutInfo.getDisplayFeatures().isEmpty()); 240 } 241 242 /** 243 * Checks that display features are consistent across portrait and landscape orientations. 244 * It is possible for the display features to be different between portrait and landscape 245 * orientations because only display features within the activity bounds are provided to the 246 * activity and the activity may be letterboxed if orientation requests are ignored. So, only 247 * check that display features that are within both portrait and landscape activity bounds 248 * are consistent. To be consistent, the feature bounds must be the same (potentially rotated if 249 * orientation requests are respected) and their type and state must be the same. 250 */ assertEqualWindowLayoutInfo( @onNull WindowLayoutInfo portraitWindowLayoutInfo, @NonNull WindowLayoutInfo landscapeWindowLayoutInfo, @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, boolean doesDisplayRotateForOrientation)251 public static void assertEqualWindowLayoutInfo( 252 @NonNull WindowLayoutInfo portraitWindowLayoutInfo, 253 @NonNull WindowLayoutInfo landscapeWindowLayoutInfo, 254 @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, 255 boolean doesDisplayRotateForOrientation) { 256 // Compute the portrait and landscape features that are within both the portrait and 257 // landscape activity bounds. 258 final List<DisplayFeature> portraitFeaturesWithinBoth = getMutualDisplayFeatures( 259 portraitWindowLayoutInfo, portraitBounds, landscapeBounds); 260 List<DisplayFeature> landscapeFeaturesWithinBoth = getMutualDisplayFeatures( 261 landscapeWindowLayoutInfo, landscapeBounds, portraitBounds); 262 assertEquals(portraitFeaturesWithinBoth.size(), landscapeFeaturesWithinBoth.size()); 263 final int nFeatures = portraitFeaturesWithinBoth.size(); 264 if (nFeatures == 0) { 265 return; 266 } 267 268 // If the display rotates to respect orientation, then to make the landscape display 269 // features comparable to the portrait display features rotate the landscape features. 270 if (doesDisplayRotateForOrientation) { 271 landscapeFeaturesWithinBoth = landscapeFeaturesWithinBoth 272 .stream() 273 .map(d -> { 274 if (!(d instanceof FoldingFeature)) { 275 return d; 276 } 277 final FoldingFeature f = (FoldingFeature) d; 278 final Rect oldBounds = d.getBounds(); 279 // Rotate the bounds by 90 degrees 280 final Rect newBounds = new Rect(oldBounds.top, oldBounds.left, 281 oldBounds.bottom, oldBounds.right); 282 return new FoldingFeature(newBounds, f.getType(), f.getState()); 283 }) 284 .collect(Collectors.toList()); 285 } 286 287 // Check that the list of features are the same 288 final boolean[] portraitFeatureMatched = new boolean[nFeatures]; 289 final boolean[] landscapeFeatureMatched = new boolean[nFeatures]; 290 for (int portraitIndex = 0; portraitIndex < nFeatures; portraitIndex++) { 291 if (portraitFeatureMatched[portraitIndex]) { 292 // A match has already been found for this portrait display feature 293 continue; 294 } 295 final DisplayFeature portraitDisplayFeature = portraitFeaturesWithinBoth 296 .get(portraitIndex); 297 for (int landscapeIndex = 0; landscapeIndex < nFeatures; landscapeIndex++) { 298 if (landscapeFeatureMatched[landscapeIndex]) { 299 // A match has already been found for this landscape display feature 300 continue; 301 } 302 final DisplayFeature landscapeDisplayFeature = landscapeFeaturesWithinBoth 303 .get(landscapeIndex); 304 // Only continue comparing if both display features are the same type of display 305 // feature (e.g. FoldingFeature) and they have the same bounds 306 if (!portraitDisplayFeature.getClass().equals(landscapeDisplayFeature.getClass()) 307 || !portraitDisplayFeature.getBounds().equals( 308 landscapeDisplayFeature.getBounds())) { 309 continue; 310 } 311 // If both are folding features, then only continue comparing if the type and state 312 // match 313 if (portraitDisplayFeature instanceof FoldingFeature) { 314 FoldingFeature portraitFoldingFeature = (FoldingFeature) portraitDisplayFeature; 315 FoldingFeature landscapeFoldingFeature = 316 (FoldingFeature) landscapeDisplayFeature; 317 if (portraitFoldingFeature.getType() != landscapeFoldingFeature.getType() 318 || portraitFoldingFeature.getState() 319 != landscapeFoldingFeature.getState()) { 320 continue; 321 } 322 } 323 // The display features match 324 portraitFeatureMatched[portraitIndex] = true; 325 landscapeFeatureMatched[landscapeIndex] = true; 326 } 327 } 328 329 // Check that a match was found for each display feature 330 for (int i = 0; i < nFeatures; i++) { 331 assertTrue(portraitFeatureMatched[i] && landscapeFeatureMatched[i]); 332 } 333 } 334 335 /** 336 * Returns the subset of {@code windowLayoutInfo} display features that are shared by the 337 * activity bounds in the current orientation and the activity bounds in the other orientation. 338 */ getMutualDisplayFeatures( @onNull WindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds, @NonNull Rect otherOrientationBounds)339 private static List<DisplayFeature> getMutualDisplayFeatures( 340 @NonNull WindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds, 341 @NonNull Rect otherOrientationBounds) { 342 return windowLayoutInfo 343 .getDisplayFeatures() 344 .stream() 345 .map(d -> { 346 if (!(d instanceof FoldingFeature)) { 347 return d; 348 } 349 // The display features are positioned relative to the activity bounds, so 350 // re-position them absolutely within the task. 351 final FoldingFeature f = (FoldingFeature) d; 352 final Rect r = f.getBounds(); 353 r.offset(currentOrientationBounds.left, currentOrientationBounds.top); 354 return new FoldingFeature(r, f.getType(), f.getState()); 355 }) 356 .filter(d -> otherOrientationBounds.contains(d.getBounds())) 357 .collect(Collectors.toList()); 358 } 359 360 /** 361 * Returns the {@link WindowAreaComponent} available in {@link WindowExtensions} if available. 362 * If the component is not available, returns null. 363 */ 364 @Nullable 365 public static WindowAreaComponent getExtensionWindowAreaComponent() { 366 final WindowExtensions extension = getWindowExtensions(); 367 return extension != null 368 ? extension.getWindowAreaComponent() 369 : null; 370 } 371 } 372