• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 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.util.Log;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 import androidx.annotation.UiContext;
33 import androidx.window.extensions.WindowExtensions;
34 import androidx.window.extensions.WindowExtensionsProvider;
35 import androidx.window.extensions.area.WindowAreaComponent;
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.List;
42 import java.util.stream.Collectors;
43 
44 /**
45  * Utility class for extensions tests, providing methods for checking if a device supports
46  * extensions, retrieving and validating the extension version, and getting the instance of
47  * {@link WindowExtensions}.
48  */
49 public class ExtensionUtil {
50 
51     private static final String EXTENSION_TAG = "Extension";
52 
53     public static final Version EXTENSION_VERSION_1 = new Version(1, 0, 0, "");
54 
55     public static final Version EXTENSION_VERSION_2 = new Version(1, 1, 0, "");
56 
57     @NonNull
getExtensionVersion()58     public static Version getExtensionVersion() {
59         try {
60             WindowExtensions extensions = getWindowExtensions();
61             if (extensions != null) {
62                 return new Version(extensions.getVendorApiLevel() /* major */, 0 /* minor */,
63                         0 /* patch */, "" /* description */);
64             }
65         } catch (NoClassDefFoundError e) {
66             Log.d(EXTENSION_TAG, "Extension version not found");
67         } catch (UnsupportedOperationException e) {
68             Log.d(EXTENSION_TAG, "Stub Extension");
69         }
70         return Version.UNKNOWN;
71     }
72 
isExtensionVersionAtLeast(Version targetVersion)73     public static boolean isExtensionVersionAtLeast(Version targetVersion) {
74         final Version version = getExtensionVersion();
75         return version.compareTo(targetVersion) >= 0;
76     }
77 
78     /**
79      * If called on a device with the vendor api level less than the bound then the test will be
80      * ignored.
81      * @param vendorApiLevel minimum {@link WindowExtensions#getVendorApiLevel()} for a test to
82      *                       succeed
83      */
assumeVendorApiLevelAtLeast(int vendorApiLevel)84     public static void assumeVendorApiLevelAtLeast(int vendorApiLevel) {
85         final Version version = getExtensionVersion();
86         assumeTrue(
87                 "Needs vendorApiLevel " + vendorApiLevel + " but has " + version.getMajor(),
88                 version.getMajor() >= vendorApiLevel
89         );
90     }
91 
isExtensionVersionValid()92     public static boolean isExtensionVersionValid() {
93         final Version version = getExtensionVersion();
94         // Check that the extension version on the device is at least the minimum valid version.
95         return version.compareTo(EXTENSION_VERSION_1) >= 0;
96     }
97 
98     @Nullable
getWindowExtensions()99     public static WindowExtensions getWindowExtensions() {
100         try {
101             return WindowExtensionsProvider.getWindowExtensions();
102         } catch (NoClassDefFoundError e) {
103             Log.d(EXTENSION_TAG, "Extension implementation not found");
104         } catch (UnsupportedOperationException e) {
105             Log.d(EXTENSION_TAG, "Stub Extension");
106         }
107         return null;
108     }
109 
assumeExtensionSupportedDevice()110     public static void assumeExtensionSupportedDevice() {
111         final boolean extensionNotNull = getWindowExtensions() != null;
112         assumeTrue("Device does not support extensions", extensionNotNull);
113         // If extensions are on the device, make sure that the version is valid.
114         assertTrue("Extension version is invalid, must be at least "
115                 + EXTENSION_VERSION_1.toString(), isExtensionVersionValid());
116     }
117 
118     @Nullable
getExtensionWindowLayoutComponent()119     public static WindowLayoutComponent getExtensionWindowLayoutComponent() {
120         WindowExtensions extension = getWindowExtensions();
121         if (extension == null) {
122             return null;
123         }
124         return extension.getWindowLayoutComponent();
125     }
126 
127     /**
128      * Publishes a WindowLayoutInfo update to a test consumer. In EXTENSION_VERSION_1, only type
129      * Activity can be the listener to WindowLayoutInfo changes. This method should be called at
130      * most once for each given Activity because addWindowLayoutInfoListener implementation
131      * assumes a 1-1 mapping between the activity and consumer.
132      */
133     @Nullable
getExtensionWindowLayoutInfo(Activity activity)134     public static WindowLayoutInfo getExtensionWindowLayoutInfo(Activity activity)
135             throws InterruptedException {
136         WindowLayoutComponent windowLayoutComponent = getExtensionWindowLayoutComponent();
137         if (windowLayoutComponent == null) {
138             return null;
139         }
140         TestValueCountJavaConsumer<WindowLayoutInfo> windowLayoutInfoConsumer =
141                 new TestValueCountJavaConsumer<>();
142         windowLayoutComponent.addWindowLayoutInfoListener(activity, windowLayoutInfoConsumer);
143         WindowLayoutInfo info = windowLayoutInfoConsumer.waitAndGet();
144 
145         // The default implementation only allows a single listener per activity. Since we are using
146         // a local windowLayoutInfoConsumer within this function, we must remember to clean up.
147         // Otherwise, subsequent calls to addWindowLayoutInfoListener with the same activity will
148         // fail to have its callback registered.
149         windowLayoutComponent.removeWindowLayoutInfoListener(windowLayoutInfoConsumer);
150         return info;
151     }
152 
153     /**
154      * Publishes a WindowLayoutInfo update to a test consumer. In EXTENSION_VERSION_2 both type
155      * WindowContext and Activity can be listeners. This method should be called at most once for
156      * each given Context because addWindowLayoutInfoListener implementation assumes a 1-1
157      * mapping between the context and consumer.
158      */
159     @Nullable
getExtensionWindowLayoutInfo(@iContext Context context)160     public static WindowLayoutInfo getExtensionWindowLayoutInfo(@UiContext Context context)
161             throws InterruptedException {
162         assertTrue(isExtensionVersionAtLeast(EXTENSION_VERSION_2));
163         WindowLayoutComponent windowLayoutComponent = getExtensionWindowLayoutComponent();
164         if (windowLayoutComponent == null) {
165             return null;
166         }
167         TestValueCountConsumer<WindowLayoutInfo> windowLayoutInfoConsumer =
168                 new TestValueCountConsumer<>();
169         windowLayoutComponent.addWindowLayoutInfoListener(context, windowLayoutInfoConsumer);
170         WindowLayoutInfo info = windowLayoutInfoConsumer.waitAndGet();
171 
172         // The default implementation only allows a single listener per context. Since we are using
173         // a local windowLayoutInfoConsumer within this function, we must remember to clean up.
174         // Otherwise, subsequent calls to addWindowLayoutInfoListener with the same context will
175         // fail to have its callback registered.
176         windowLayoutComponent.removeWindowLayoutInfoListener(windowLayoutInfoConsumer);
177         return info;
178     }
179 
180     @NonNull
getExtensionDisplayFeatureTypes(Activity activity)181     public static int[] getExtensionDisplayFeatureTypes(Activity activity)
182             throws InterruptedException {
183         WindowLayoutInfo windowLayoutInfo = getExtensionWindowLayoutInfo(activity);
184         if (windowLayoutInfo == null) {
185             return new int[0];
186         }
187         List<DisplayFeature> displayFeatureList = windowLayoutInfo.getDisplayFeatures();
188         return displayFeatureList
189                 .stream()
190                 .filter(d -> d instanceof FoldingFeature)
191                 .map(d -> ((FoldingFeature) d).getType())
192                 .mapToInt(i -> i.intValue())
193                 .toArray();
194     }
195 
196     /**
197      * Returns whether the device reports at least one display feature.
198      */
assumeHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo)199     public static void assumeHasDisplayFeatures(WindowLayoutInfo windowLayoutInfo) {
200         // If WindowLayoutComponent is implemented, then WindowLayoutInfo and the list of display
201         // features cannot be null. However the list can be empty if the device does not report
202         // any display features.
203         assertNotNull(windowLayoutInfo);
204         assertNotNull(windowLayoutInfo.getDisplayFeatures());
205         assumeFalse(windowLayoutInfo.getDisplayFeatures().isEmpty());
206     }
207 
208     /**
209      * Checks that display features are consistent across portrait and landscape orientations.
210      * It is possible for the display features to be different between portrait and landscape
211      * orientations because only display features within the activity bounds are provided to the
212      * activity and the activity may be letterboxed if orientation requests are ignored. So, only
213      * check that display features that are within both portrait and landscape activity bounds
214      * are consistent. To be consistent, the feature bounds must be the same (potentially rotated if
215      * orientation requests are respected) and their type and state must be the same.
216      */
assertEqualWindowLayoutInfo( @onNull WindowLayoutInfo portraitWindowLayoutInfo, @NonNull WindowLayoutInfo landscapeWindowLayoutInfo, @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds, boolean doesDisplayRotateForOrientation)217     public static void assertEqualWindowLayoutInfo(
218             @NonNull WindowLayoutInfo portraitWindowLayoutInfo,
219             @NonNull WindowLayoutInfo landscapeWindowLayoutInfo,
220             @NonNull Rect portraitBounds, @NonNull Rect landscapeBounds,
221             boolean doesDisplayRotateForOrientation) {
222         // Compute the portrait and landscape features that are within both the portrait and
223         // landscape activity bounds.
224         final List<DisplayFeature> portraitFeaturesWithinBoth = getMutualDisplayFeatures(
225                 portraitWindowLayoutInfo, portraitBounds, landscapeBounds);
226         List<DisplayFeature> landscapeFeaturesWithinBoth = getMutualDisplayFeatures(
227                 landscapeWindowLayoutInfo, landscapeBounds, portraitBounds);
228         assertEquals(portraitFeaturesWithinBoth.size(), landscapeFeaturesWithinBoth.size());
229         final int nFeatures = portraitFeaturesWithinBoth.size();
230         if (nFeatures == 0) {
231             return;
232         }
233 
234         // If the display rotates to respect orientation, then to make the landscape display
235         // features comparable to the portrait display features rotate the landscape features.
236         if (doesDisplayRotateForOrientation) {
237             landscapeFeaturesWithinBoth = landscapeFeaturesWithinBoth
238                     .stream()
239                     .map(d -> {
240                         if (!(d instanceof FoldingFeature)) {
241                             return d;
242                         }
243                         final FoldingFeature f = (FoldingFeature) d;
244                         final Rect oldBounds = d.getBounds();
245                         // Rotate the bounds by 90 degrees
246                         final Rect newBounds = new Rect(oldBounds.top, oldBounds.left,
247                                 oldBounds.bottom, oldBounds.right);
248                         return new FoldingFeature(newBounds, f.getType(), f.getState());
249                     })
250                     .collect(Collectors.toList());
251         }
252 
253         // Check that the list of features are the same
254         final boolean[] portraitFeatureMatched = new boolean[nFeatures];
255         final boolean[] landscapeFeatureMatched = new boolean[nFeatures];
256         for (int portraitIndex = 0; portraitIndex < nFeatures; portraitIndex++) {
257             if (portraitFeatureMatched[portraitIndex]) {
258                 // A match has already been found for this portrait display feature
259                 continue;
260             }
261             final DisplayFeature portraitDisplayFeature = portraitFeaturesWithinBoth
262                     .get(portraitIndex);
263             for (int landscapeIndex = 0; landscapeIndex < nFeatures; landscapeIndex++) {
264                 if (landscapeFeatureMatched[landscapeIndex]) {
265                     // A match has already been found for this landscape display feature
266                     continue;
267                 }
268                 final DisplayFeature landscapeDisplayFeature = landscapeFeaturesWithinBoth
269                         .get(landscapeIndex);
270                 // Only continue comparing if both display features are the same type of display
271                 // feature (e.g. FoldingFeature) and they have the same bounds
272                 if (!portraitDisplayFeature.getClass().equals(landscapeDisplayFeature.getClass())
273                         || !portraitDisplayFeature.getBounds().equals(
274                                 landscapeDisplayFeature.getBounds())) {
275                     continue;
276                 }
277                 // If both are folding features, then only continue comparing if the type and state
278                 // match
279                 if (portraitDisplayFeature instanceof FoldingFeature) {
280                     FoldingFeature portraitFoldingFeature = (FoldingFeature) portraitDisplayFeature;
281                     FoldingFeature landscapeFoldingFeature =
282                             (FoldingFeature) landscapeDisplayFeature;
283                     if (portraitFoldingFeature.getType() != landscapeFoldingFeature.getType()
284                             || portraitFoldingFeature.getState()
285                             != landscapeFoldingFeature.getState()) {
286                         continue;
287                     }
288                 }
289                 // The display features match
290                 portraitFeatureMatched[portraitIndex] = true;
291                 landscapeFeatureMatched[landscapeIndex] = true;
292             }
293         }
294 
295         // Check that a match was found for each display feature
296         for (int i = 0; i < nFeatures; i++) {
297             assertTrue(portraitFeatureMatched[i] && landscapeFeatureMatched[i]);
298         }
299     }
300 
301     /**
302      * Returns the subset of {@param windowLayoutInfo} display features that are shared by the
303      * activity bounds in the current orientation and the activity bounds in the other orientation.
304      */
getMutualDisplayFeatures( @onNull WindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds, @NonNull Rect otherOrientationBounds)305     private static List<DisplayFeature> getMutualDisplayFeatures(
306             @NonNull WindowLayoutInfo windowLayoutInfo, @NonNull Rect currentOrientationBounds,
307             @NonNull Rect otherOrientationBounds) {
308         return windowLayoutInfo
309                 .getDisplayFeatures()
310                 .stream()
311                 .map(d -> {
312                     if (!(d instanceof FoldingFeature)) {
313                         return d;
314                     }
315                     // The display features are positioned relative to the activity bounds, so
316                     // re-position them absolutely within the task.
317                     final FoldingFeature f = (FoldingFeature) d;
318                     final Rect r = f.getBounds();
319                     r.offset(currentOrientationBounds.left, currentOrientationBounds.top);
320                     return new FoldingFeature(r, f.getType(), f.getState());
321                 })
322                 .filter(d -> otherOrientationBounds.contains(d.getBounds()))
323                 .collect(Collectors.toList());
324     }
325 
326     /**
327      * Returns the {@link WindowAreaComponent} available in {@link WindowExtensions} if available.
328      * If the component is not available, returns null.
329      */
330     @Nullable
331     public static WindowAreaComponent getExtensionWindowAreaComponent() {
332         WindowExtensions extension = getWindowExtensions();
333         if (extension == null || extension.getVendorApiLevel() < 2) {
334             return null;
335         }
336         return extension.getWindowAreaComponent();
337     }
338 }
339