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