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