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