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.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.os.IBinder; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.window.extensions.layout.WindowLayoutInfo; 34 import androidx.window.sidecar.SidecarDisplayFeature; 35 import androidx.window.sidecar.SidecarInterface; 36 import androidx.window.sidecar.SidecarProvider; 37 import androidx.window.sidecar.SidecarWindowLayoutInfo; 38 39 import java.util.List; 40 import java.util.stream.Collectors; 41 42 /** 43 * Utility class for sidecar tests, providing methods for checking if a device supports sidecar, 44 * retrieving and validating the sidecar version, and getting the instance of 45 * {@link SidecarInterface}. 46 */ 47 public class SidecarUtil { 48 49 private static final String SIDECAR_TAG = "Sidecar"; 50 51 public static final Version MINIMUM_SIDECAR_VERSION = new Version(1, 0, 0, ""); 52 53 /** 54 * Returns the Sidecar version in a semantic version format. 55 */ 56 @SuppressWarnings("deprecation") 57 @NonNull getSidecarVersion()58 public static Version getSidecarVersion() { 59 try { 60 String sidecarVersionStr = SidecarProvider.getApiVersion(); 61 if (Version.isValidVersion(sidecarVersionStr)) { 62 return Version.parse(sidecarVersionStr); 63 } 64 } catch (NoClassDefFoundError e) { 65 Log.d(SIDECAR_TAG, "Sidecar version not found"); 66 } catch (UnsupportedOperationException e) { 67 Log.d(SIDECAR_TAG, "Stub Sidecar"); 68 } 69 return Version.UNKNOWN; 70 } 71 72 /** 73 * Returns true if the sidecar version is greater than 0. 74 */ isSidecarVersionValid()75 public static boolean isSidecarVersionValid() { 76 final Version version = getSidecarVersion(); 77 // Check that the sidecar version on the device is at least the minimum valid version. 78 return version.compareTo(MINIMUM_SIDECAR_VERSION) >= 0; 79 } 80 81 /** 82 * Returns the current {@link SidecarInterface} if present on the device. 83 */ 84 @SuppressWarnings("deprecation") 85 @Nullable getSidecarInterface(Context context)86 public static SidecarInterface getSidecarInterface(Context context) { 87 try { 88 return SidecarProvider.getSidecarImpl(context); 89 } catch (NoClassDefFoundError e) { 90 Log.d(SIDECAR_TAG, "Sidecar implementation not found"); 91 } catch (UnsupportedOperationException e) { 92 Log.d(SIDECAR_TAG, "Stub Sidecar"); 93 } 94 return null; 95 } 96 97 /** 98 * Assumes that sidecar is present and has a version above 0. 99 */ assumeSidecarSupportedDevice(Context context)100 public static void assumeSidecarSupportedDevice(Context context) { 101 final boolean sidecarInterfaceNotNull = getSidecarInterface(context) != null; 102 assumeTrue("Device does not support sidecar", sidecarInterfaceNotNull); 103 // If sidecar is on the device, make sure that the version is valid. 104 assertTrue("Sidecar version is invalid, must be at least " 105 + MINIMUM_SIDECAR_VERSION.toString(), isSidecarVersionValid()); 106 } 107 108 /** 109 * Returns an int array containing the raw values of the currently visible fold types. 110 * @param activity An {@link Activity} that is visible and intersects the folds 111 * @return an int array containing the raw values for the current visible fold types. 112 * @throws InterruptedException when the async collection of the {@link WindowLayoutInfo} 113 * is interrupted. 114 */ 115 @SuppressWarnings("deprecation") // SidecarInterface is deprecated but required. 116 @NonNull getSidecarDisplayFeatureTypes(Activity activity)117 public static int[] getSidecarDisplayFeatureTypes(Activity activity) { 118 SidecarInterface sidecarInterface = getSidecarInterface(activity); 119 if (sidecarInterface == null) { 120 return new int[0]; 121 } 122 SidecarWindowLayoutInfo windowLayoutInfo = sidecarInterface.getWindowLayoutInfo( 123 getActivityWindowToken(activity)); 124 if (windowLayoutInfo == null) { 125 return new int[0]; 126 } 127 return windowLayoutInfo.displayFeatures 128 .stream() 129 .map(d -> d.getType()) 130 .mapToInt(i -> i.intValue()) 131 .toArray(); 132 } 133 134 /** 135 * Assumes that the window associated to the window token has at least one display feature. 136 * @param sidecarInterface the implementation of sidecar from the device. 137 * @param windowToken the token for the active window. 138 */ 139 @SuppressWarnings("deprecation") // SidecarInterface is deprecated but required. assumeHasDisplayFeatures(SidecarInterface sidecarInterface, IBinder windowToken)140 public static void assumeHasDisplayFeatures(SidecarInterface sidecarInterface, 141 IBinder windowToken) { 142 SidecarWindowLayoutInfo windowLayoutInfo = sidecarInterface.getWindowLayoutInfo( 143 windowToken); 144 assertNotNull(windowLayoutInfo); // window layout info cannot be null 145 List<SidecarDisplayFeature> displayFeatures = windowLayoutInfo.displayFeatures; 146 assertNotNull(displayFeatures); // list cannot be null 147 assumeFalse(displayFeatures.isEmpty()); // list can be empty 148 } 149 150 /** 151 * Checks that display features are consistent across portrait and landscape orientations. 152 * It is possible for the display features to be different between portrait and landscape 153 * orientations because only display features within the activity bounds are provided to the 154 * activity and the activity may be letterboxed if orientation requests are ignored. So, only 155 * check that display features that are within both portrait and landscape activity bounds 156 * are consistent. To be consistent, the feature bounds must be the same (potentially rotated if 157 * orientation requests are respected) and their type and state must be the same. 158 */ assertEqualWindowLayoutInfo( @onNull SidecarWindowLayoutInfo portraitWindowLayoutInfo, @NonNull SidecarWindowLayoutInfo landscapeWindowLayoutInfo, @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, boolean doesDisplayRotateForOrientation)159 public static void assertEqualWindowLayoutInfo( 160 @NonNull SidecarWindowLayoutInfo portraitWindowLayoutInfo, 161 @NonNull SidecarWindowLayoutInfo landscapeWindowLayoutInfo, 162 @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, 163 boolean doesDisplayRotateForOrientation) { 164 // Compute the portrait and landscape features that are within both the portrait and 165 // landscape activity bounds. 166 final List<SidecarDisplayFeature> portraitFeaturesWithinBoth = getMutualDisplayFeatures( 167 portraitWindowLayoutInfo, portraitBounds, landscapeBounds); 168 List<SidecarDisplayFeature> landscapeFeaturesWithinBoth = getMutualDisplayFeatures( 169 landscapeWindowLayoutInfo, landscapeBounds, portraitBounds); 170 assertEquals(portraitFeaturesWithinBoth.size(), landscapeFeaturesWithinBoth.size()); 171 final int nFeatures = portraitFeaturesWithinBoth.size(); 172 if (nFeatures == 0) { 173 return; 174 } 175 176 // If the display rotates to respect orientation, then to make the landscape display 177 // features comparable to the portrait display features rotate the landscape features. 178 if (doesDisplayRotateForOrientation) { 179 landscapeFeaturesWithinBoth = landscapeFeaturesWithinBoth 180 .stream() 181 .map(d -> { 182 final Rect oldBounds = d.getRect(); 183 // Rotate the bounds by 90 degrees 184 final Rect newBounds = new Rect(oldBounds.top, oldBounds.left, 185 oldBounds.bottom, oldBounds.right); 186 SidecarDisplayFeature newDisplayFeature = new SidecarDisplayFeature(); 187 newDisplayFeature.setRect(newBounds); 188 newDisplayFeature.setType(d.getType()); 189 return newDisplayFeature; 190 }) 191 .collect(Collectors.toList()); 192 } 193 194 // Check that the list of features are the same 195 final boolean[] portraitFeatureMatched = new boolean[nFeatures]; 196 final boolean[] landscapeFeatureMatched = new boolean[nFeatures]; 197 for (int portraitIndex = 0; portraitIndex < nFeatures; portraitIndex++) { 198 if (portraitFeatureMatched[portraitIndex]) { 199 // A match has already been found for this portrait display feature 200 continue; 201 } 202 final SidecarDisplayFeature portraitDisplayFeature = portraitFeaturesWithinBoth 203 .get(portraitIndex); 204 for (int landscapeIndex = 0; landscapeIndex < nFeatures; landscapeIndex++) { 205 if (landscapeFeatureMatched[landscapeIndex]) { 206 // A match has already been found for this landscape display feature 207 continue; 208 } 209 final SidecarDisplayFeature landscapeDisplayFeature = landscapeFeaturesWithinBoth 210 .get(landscapeIndex); 211 // Check that the bounds and type match 212 if (portraitDisplayFeature.getRect().equals(landscapeDisplayFeature.getRect()) 213 && portraitDisplayFeature.getType() == landscapeDisplayFeature.getType()) { 214 // The display features match 215 portraitFeatureMatched[portraitIndex] = true; 216 landscapeFeatureMatched[landscapeIndex] = true; 217 } 218 } 219 } 220 221 // Check that a match was found for each display feature 222 for (int i = 0; i < nFeatures; i++) { 223 assertTrue(portraitFeatureMatched[i] && landscapeFeatureMatched[i]); 224 } 225 } 226 227 /** 228 * Returns the subset of {@param windowLayoutInfo} display features that are shared by the 229 * activity bounds in the current orientation and the activity bounds in the other orientation. 230 */ getMutualDisplayFeatures( @onNull SidecarWindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds, @NonNull Rect otherOrientationBounds)231 private static List<SidecarDisplayFeature> getMutualDisplayFeatures( 232 @NonNull SidecarWindowLayoutInfo windowLayoutInfo, 233 @NonNull Rect currentOrientationBounds, @NonNull Rect otherOrientationBounds) { 234 return windowLayoutInfo.displayFeatures 235 .stream() 236 .map(d -> { 237 // The display features are positioned relative to the activity bounds, so 238 // re-position them absolutely within the task. 239 final Rect newBounds = new Rect(d.getRect()); 240 newBounds.offset(currentOrientationBounds.left, currentOrientationBounds.top); 241 SidecarDisplayFeature newDisplayFeature = new SidecarDisplayFeature(); 242 newDisplayFeature.setRect(newBounds); 243 newDisplayFeature.setType(d.getType()); 244 return newDisplayFeature; 245 }) 246 .filter(d -> otherOrientationBounds.contains(d.getRect())) 247 .collect(Collectors.toList()); 248 } 249 getActivityWindowToken(Activity activity)250 private static IBinder getActivityWindowToken(Activity activity) { 251 return activity.getWindow().getAttributes().token; 252 } 253 } 254