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 android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.getActivityBounds; 20 import static android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.getMaximumActivityBounds; 21 22 import static org.junit.Assert.assertEquals; 23 import static org.junit.Assert.assertNotNull; 24 import static org.junit.Assert.assertTrue; 25 import static org.junit.Assume.assumeFalse; 26 import static org.junit.Assume.assumeTrue; 27 28 import android.app.Activity; 29 import android.graphics.Rect; 30 import android.util.Log; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.window.extensions.WindowExtensions; 35 import androidx.window.extensions.WindowExtensionsProvider; 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.ArrayList; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.concurrent.ExecutionException; 45 import java.util.concurrent.TimeoutException; 46 import java.util.stream.Collectors; 47 48 /** 49 * Utility class for extensions tests, providing methods for checking if a device supports 50 * extensions, retrieving and validating the extension version, and getting the instance of 51 * {@link WindowExtensions}. 52 */ 53 public class ExtensionUtil { 54 55 private static final String EXTENSION_TAG = "Extension"; 56 57 public static final Version MINIMUM_EXTENSION_VERSION = new Version(1, 0, 0, ""); 58 59 @NonNull getExtensionVersion()60 public static Version getExtensionVersion() { 61 try { 62 WindowExtensions extensions = getWindowExtensions(); 63 if (extensions != null) { 64 return new Version(extensions.getVendorApiLevel() /* major */, 0 /* minor */, 65 0 /* patch */, "" /* description */); 66 } 67 } catch (NoClassDefFoundError e) { 68 Log.d(EXTENSION_TAG, "Extension version not found"); 69 } catch (UnsupportedOperationException e) { 70 Log.d(EXTENSION_TAG, "Stub Extension"); 71 } 72 return Version.UNKNOWN; 73 } 74 isExtensionVersionValid()75 public static boolean isExtensionVersionValid() { 76 final Version version = getExtensionVersion(); 77 // Check that the extension version on the device is at least the minimum valid version. 78 return version.compareTo(MINIMUM_EXTENSION_VERSION) >= 0; 79 } 80 81 @Nullable getWindowExtensions()82 public static WindowExtensions getWindowExtensions() { 83 try { 84 return WindowExtensionsProvider.getWindowExtensions(); 85 } catch (NoClassDefFoundError e) { 86 Log.d(EXTENSION_TAG, "Extension implementation not found"); 87 } catch (UnsupportedOperationException e) { 88 Log.d(EXTENSION_TAG, "Stub Extension"); 89 } 90 return null; 91 } 92 assumeExtensionSupportedDevice()93 public static void assumeExtensionSupportedDevice() { 94 final boolean extensionNotNull = getWindowExtensions() != null; 95 assumeTrue("Device does not support extensions", extensionNotNull); 96 // If extensions are on the device, make sure that the version is valid. 97 assertTrue("Extension version is invalid, must be at least " 98 + MINIMUM_EXTENSION_VERSION.toString(), isExtensionVersionValid()); 99 } 100 101 @Nullable getExtensionWindowLayoutComponent()102 public static WindowLayoutComponent getExtensionWindowLayoutComponent() { 103 WindowExtensions extension = getWindowExtensions(); 104 if (extension == null) { 105 return null; 106 } 107 return extension.getWindowLayoutComponent(); 108 } 109 110 @Nullable getExtensionWindowLayoutInfo(Activity activity)111 public static WindowLayoutInfo getExtensionWindowLayoutInfo(Activity activity) 112 throws ExecutionException, InterruptedException, TimeoutException { 113 WindowLayoutComponent windowLayoutComponent = getExtensionWindowLayoutComponent(); 114 if (windowLayoutComponent == null) { 115 return null; 116 } 117 TestValueCountConsumer<WindowLayoutInfo> windowLayoutInfoConsumer = 118 new TestValueCountConsumer<>(); 119 windowLayoutComponent.addWindowLayoutInfoListener(activity, windowLayoutInfoConsumer); 120 return windowLayoutInfoConsumer.waitAndGet(); 121 } 122 123 @NonNull getExtensionDisplayFeatureTypes(Activity activity)124 public static int[] getExtensionDisplayFeatureTypes(Activity activity) 125 throws ExecutionException, InterruptedException, TimeoutException { 126 WindowLayoutInfo windowLayoutInfo = getExtensionWindowLayoutInfo(activity); 127 if (windowLayoutInfo == null) { 128 return new int[0]; 129 } 130 List<DisplayFeature> displayFeatureList = windowLayoutInfo.getDisplayFeatures(); 131 return displayFeatureList 132 .stream() 133 .filter(d -> d instanceof FoldingFeature) 134 .map(d -> ((FoldingFeature) d).getType()) 135 .mapToInt(i -> i.intValue()) 136 .toArray(); 137 } 138 139 /** 140 * Returns whether the device reports at least one display feature. 141 */ assumeHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo)142 public static void assumeHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo) { 143 // If WindowLayoutComponent is implemented, then WindowLayoutInfo and the list of display 144 // features cannot be null. However the list can be empty if the device does not report 145 // any display features. 146 assertNotNull(windowLayoutInfo); 147 assertNotNull(windowLayoutInfo.getDisplayFeatures()); 148 assumeFalse(windowLayoutInfo.getDisplayFeatures().isEmpty()); 149 } 150 151 /** 152 * Checks that display features are consistent across portrait and landscape orientations. 153 * It is possible for the display features to be different between portrait and landscape 154 * orientations because only display features within the activity bounds are provided to the 155 * activity and the activity may be letterboxed if orientation requests are ignored. So, only 156 * check that display features that are within both portrait and landscape activity bounds 157 * are consistent. To be consistent, the feature bounds must be the same (potentially rotated if 158 * orientation requests are respected) and their type and state must be the same. 159 */ assertEqualWindowLayoutInfo( @onNull WindowLayoutInfo portraitWindowLayoutInfo, @NonNull WindowLayoutInfo landscapeWindowLayoutInfo, @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, boolean doesDisplayRotateForOrientation)160 public static void assertEqualWindowLayoutInfo( 161 @NonNull WindowLayoutInfo portraitWindowLayoutInfo, 162 @NonNull WindowLayoutInfo landscapeWindowLayoutInfo, 163 @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, 164 boolean doesDisplayRotateForOrientation) { 165 // Compute the portrait and landscape features that are within both the portrait and 166 // landscape activity bounds. 167 final List<DisplayFeature> portraitFeaturesWithinBoth = getMutualDisplayFeatures( 168 portraitWindowLayoutInfo, portraitBounds, landscapeBounds); 169 List<DisplayFeature> landscapeFeaturesWithinBoth = getMutualDisplayFeatures( 170 landscapeWindowLayoutInfo, landscapeBounds, portraitBounds); 171 assertEquals(portraitFeaturesWithinBoth.size(), landscapeFeaturesWithinBoth.size()); 172 final int nFeatures = portraitFeaturesWithinBoth.size(); 173 if (nFeatures == 0) { 174 return; 175 } 176 177 // If the display rotates to respect orientation, then to make the landscape display 178 // features comparable to the portrait display features rotate the landscape features. 179 if (doesDisplayRotateForOrientation) { 180 landscapeFeaturesWithinBoth = landscapeFeaturesWithinBoth 181 .stream() 182 .map(d -> { 183 if (!(d instanceof FoldingFeature)) { 184 return d; 185 } 186 final FoldingFeature f = (FoldingFeature) d; 187 final Rect oldBounds = d.getBounds(); 188 // Rotate the bounds by 90 degrees 189 final Rect newBounds = new Rect(oldBounds.top, oldBounds.left, 190 oldBounds.bottom, oldBounds.right); 191 return new FoldingFeature(newBounds, f.getType(), f.getState()); 192 }) 193 .collect(Collectors.toList()); 194 } 195 196 // Check that the list of features are the same 197 final boolean[] portraitFeatureMatched = new boolean[nFeatures]; 198 final boolean[] landscapeFeatureMatched = new boolean[nFeatures]; 199 for (int portraitIndex = 0; portraitIndex < nFeatures; portraitIndex++) { 200 if (portraitFeatureMatched[portraitIndex]) { 201 // A match has already been found for this portrait display feature 202 continue; 203 } 204 final DisplayFeature portraitDisplayFeature = portraitFeaturesWithinBoth 205 .get(portraitIndex); 206 for (int landscapeIndex = 0; landscapeIndex < nFeatures; landscapeIndex++) { 207 if (landscapeFeatureMatched[landscapeIndex]) { 208 // A match has already been found for this landscape display feature 209 continue; 210 } 211 final DisplayFeature landscapeDisplayFeature = landscapeFeaturesWithinBoth 212 .get(landscapeIndex); 213 // Only continue comparing if both display features are the same type of display 214 // feature (e.g. FoldingFeature) and they have the same bounds 215 if (!portraitDisplayFeature.getClass().equals(landscapeDisplayFeature.getClass()) 216 || !portraitDisplayFeature.getBounds().equals( 217 landscapeDisplayFeature.getBounds())) { 218 continue; 219 } 220 // If both are folding features, then only continue comparing if the type and state 221 // match 222 if (portraitDisplayFeature instanceof FoldingFeature) { 223 FoldingFeature portraitFoldingFeature = (FoldingFeature) portraitDisplayFeature; 224 FoldingFeature landscapeFoldingFeature = 225 (FoldingFeature) landscapeDisplayFeature; 226 if (portraitFoldingFeature.getType() != landscapeFoldingFeature.getType() 227 || portraitFoldingFeature.getState() 228 != landscapeFoldingFeature.getState()) { 229 continue; 230 } 231 } 232 // The display features match 233 portraitFeatureMatched[portraitIndex] = true; 234 landscapeFeatureMatched[landscapeIndex] = true; 235 } 236 } 237 238 // Check that a match was found for each display feature 239 for (int i = 0; i < nFeatures; i++) { 240 assertTrue(portraitFeatureMatched[i] && landscapeFeatureMatched[i]); 241 } 242 } 243 244 /** 245 * Returns the subset of {@param windowLayoutInfo} display features that are shared by the 246 * activity bounds in the current orientation and the activity bounds in the other orientation. 247 */ getMutualDisplayFeatures( @onNull WindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds, @NonNull Rect otherOrientationBounds)248 private static List<DisplayFeature> getMutualDisplayFeatures( 249 @NonNull WindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds, 250 @NonNull Rect otherOrientationBounds) { 251 return windowLayoutInfo 252 .getDisplayFeatures() 253 .stream() 254 .map(d -> { 255 if (!(d instanceof FoldingFeature)) { 256 return d; 257 } 258 // The display features are positioned relative to the activity bounds, so 259 // re-position them absolutely within the task. 260 final FoldingFeature f = (FoldingFeature) d; 261 final Rect r = f.getBounds(); 262 r.offset(currentOrientationBounds.left, currentOrientationBounds.top); 263 return new FoldingFeature(r, f.getType(), f.getState()); 264 }) 265 .filter(d -> otherOrientationBounds.contains(d.getBounds())) 266 .collect(Collectors.toList()); 267 } 268 } 269