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