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