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