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