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