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