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