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