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.app.cts.wallpapers;
18 
19 import static android.app.WallpaperManager.FLAG_LOCK;
20 import static android.app.WallpaperManager.FLAG_SYSTEM;
21 
22 import static com.google.common.truth.Truth.assertThat;
23 
24 import android.app.WallpaperColors;
25 import android.app.WallpaperManager;
26 import android.content.ComponentName;
27 import android.os.Handler;
28 import android.util.Log;
29 
30 import androidx.annotation.Nullable;
31 
32 import com.android.compatibility.common.util.ThrowingRunnable;
33 
34 import java.io.IOException;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.concurrent.CountDownLatch;
38 import java.util.concurrent.TimeUnit;
39 import java.util.stream.Stream;
40 
41 public class WallpaperManagerTestUtils {
42 
43     private static final String TAG = "WallpaperManagerTest";
44 
45     public static final ComponentName TEST_LIVE_WALLPAPER_COMPONENT = new ComponentName(
46             TestLiveWallpaper.class.getPackageName(),
47             TestLiveWallpaper.class.getName());
48 
49     public static final ComponentName TEST_LIVE_WALLPAPER_NO_UNFOLD_COMPONENT = new ComponentName(
50             TestLiveWallpaperNoUnfoldTransition.class.getPackageName(),
51             TestLiveWallpaperNoUnfoldTransition.class.getName());
52 
53     public static final ComponentName TEST_LIVE_WALLPAPER_AMBIENT_COMPONENT = new ComponentName(
54             TestLiveWallpaperSupportingAmbientMode.class.getPackageName(),
55             TestLiveWallpaperSupportingAmbientMode.class.getName());
56 
57     /**
58      * Runs the given runnable and waits until request number of engine events are triggered.
59      *
60      * @param timeout              Amount of time to wait.
61      * @param unit                 Unit for the timeout.
62      * @param onCreateCount        Number of times Engine::onCreate is expected to be called.
63      * @param onDestroyCount       Number of times Engine::onDestroy is expected to be called.
64      * @param surfaceCreationCount Number of times Engine::onSurfaceChanged is expected to be
65      *                             called.
66      * @param runnable             The action to perform that will trigger engine events.
67      * @return True if all events were received, false if there was a timeout waiting for any of
68      * the events.
69      */
runAndAwaitChanges(long timeout, TimeUnit unit, int onCreateCount, int onDestroyCount, int surfaceCreationCount, ThrowingRunnable runnable)70     public static boolean runAndAwaitChanges(long timeout, TimeUnit unit,
71             int onCreateCount, int onDestroyCount, int surfaceCreationCount,
72             ThrowingRunnable runnable) {
73         TestWallpaperService.EngineCallbackCountdown callback =
74                 new TestWallpaperService.EngineCallbackCountdown(onCreateCount, onDestroyCount,
75                         surfaceCreationCount);
76         TestWallpaperService.Companion.addCallback(callback);
77         boolean result;
78         try {
79             runnable.run();
80             result = callback.awaitEvents(timeout, unit);
81         } catch (Exception e) {
82             throw new RuntimeException("Caught exception", e);
83         } finally {
84             TestWallpaperService.Companion.removeCallback(callback);
85         }
86 
87         return result;
88     }
89 
90     /**
91      * Runs the given runnable and waits until request number of color change events are triggered.
92      * @param timeout          Amount of time to wait.
93      * @param unit             Unit for the timeout.
94      * @param whichColors      Flags for which we expect a color change callback.
95      * @param wallpaperManager Instance of WallpaperManager that will register the color listener.
96      * @param handler          Handler that will receive the color callbacks.
97      * @param runnable         The action to perform that will trigger engine events.
98      * @return True if all events were received, false if there was a timeout waiting for any of
99      * the events.
100      *
101      * @see #runAndAwaitChanges(long, TimeUnit, int, int, int, ThrowingRunnable)
102      */
runAndAwaitColorChanges(long timeout, TimeUnit unit, int whichColors, WallpaperManager wallpaperManager, Handler handler, ThrowingRunnable runnable)103     public static boolean runAndAwaitColorChanges(long timeout, TimeUnit unit,
104             int whichColors, WallpaperManager wallpaperManager, Handler handler,
105             ThrowingRunnable runnable) {
106         assertThat(whichColors).isIn(List.of(FLAG_LOCK, FLAG_SYSTEM, FLAG_LOCK | FLAG_SYSTEM));
107         ColorChangeWaiter callback = new ColorChangeWaiter(whichColors);
108         wallpaperManager.addOnColorsChangedListener(callback, handler);
109         boolean result;
110         try {
111             runnable.run();
112             result = callback.waitForChanges(timeout, unit);
113         } catch (Exception e) {
114             throw new RuntimeException("Caught exception", e);
115         } finally {
116             wallpaperManager.removeOnColorsChangedListener(callback);
117         }
118         return result;
119     }
120 
121     /**
122      * enumeration of all wallpapers used for test purposes: 3 static, 3 live wallpapers:   <br>
123      * static1 <=> red bitmap <br>
124      * static2 <=> green bitmap <br>
125      * static3 <=> blue bitmap <br>
126      * <br>
127      * live1 <=> TestLiveWallpaper (cyan) <br>
128      * live2 <=> TestLiveWallpaperNoUnfoldTransition (magenta) <br>
129      * live3 <=> TestLiveWallpaperSupportingAmbientMode (yellow) <br>
130      */
131     public enum TestWallpaper {
132         STATIC1(R.drawable.icon_red, null),
133         STATIC2(R.drawable.icon_green, null),
134         STATIC3(R.drawable.icon_blue, null),
135         LIVE1(null, TEST_LIVE_WALLPAPER_COMPONENT),
136         LIVE2(null, TEST_LIVE_WALLPAPER_NO_UNFOLD_COMPONENT),
137         LIVE3(null, TEST_LIVE_WALLPAPER_AMBIENT_COMPONENT);
138 
139         private final Integer mBitmapResourceId;
140         private final ComponentName mComponentName;
141 
TestWallpaper(Integer bitmapResourceId, ComponentName componentName)142         TestWallpaper(Integer bitmapResourceId, ComponentName componentName) {
143             mBitmapResourceId = bitmapResourceId;
144             mComponentName = componentName;
145         }
146 
getBitmapResourceId()147         int getBitmapResourceId() {
148             return mBitmapResourceId;
149         }
150 
getComponentName()151         ComponentName getComponentName() {
152             return mComponentName;
153         }
154 
type()155         private String type() {
156             return isStatic() ? "static" : "live";
157         }
158 
isStatic()159         private boolean isStatic() {
160             return mComponentName == null;
161         }
162 
isLive()163         private boolean isLive() {
164             return !isStatic();
165         }
166     }
167 
allStaticTestWallpapers()168     private static List<TestWallpaper> allStaticTestWallpapers() {
169         return List.of(TestWallpaper.STATIC1, TestWallpaper.STATIC2, TestWallpaper.STATIC3);
170     }
171 
allLiveTestWallpapers()172     private static List<TestWallpaper> allLiveTestWallpapers() {
173         return List.of(TestWallpaper.LIVE1, TestWallpaper.LIVE2, TestWallpaper.LIVE3);
174     }
175 
176     public static class WallpaperChange {
177         final TestWallpaper mWallpaper;
178         int mDestination;
WallpaperChange( TestWallpaper wallpaper, int destination)179         public WallpaperChange(
180                 TestWallpaper wallpaper, int destination) {
181             this.mWallpaper = wallpaper;
182             this.mDestination = destination;
183         }
184     }
185 
186     /**
187      * Class representing a state in which our WallpaperManager may be during our tests.
188      * A state is fully represented by the wallpaper that are present on home and lock screen.
189      */
190     public enum WallpaperState {
191         LIVE_SAME_SINGLE(TestWallpaper.LIVE1, TestWallpaper.LIVE1, true),
192         LIVE_SAME_MULTI(TestWallpaper.LIVE1, TestWallpaper.LIVE1, false),
193         LIVE_DIFF_MULTI(TestWallpaper.LIVE1, TestWallpaper.LIVE2, false),
194         LIVE_STATIC_MULTI(TestWallpaper.LIVE1, TestWallpaper.STATIC1, false),
195         STATIC_SAME_SINGLE(TestWallpaper.STATIC1, TestWallpaper.STATIC1, true),
196         STATIC_SAME_MULTI(TestWallpaper.STATIC1, TestWallpaper.STATIC1, false),
197         STATIC_DIFF_MULTI(TestWallpaper.STATIC1, TestWallpaper.STATIC2, false),
198         STATIC_LIVE_MULTI(TestWallpaper.STATIC1, TestWallpaper.LIVE1, false);
199 
200         private final TestWallpaper mHomeWallpaper;
201         private final TestWallpaper mLockWallpaper;
202 
203         /**
204          * it is possible to have two copies of the same engine on home + lock screen,
205          * in which this flag would be false.
206          * True means that mHomeWallpaper == mLockWallpaper and there is only one active engine.
207          */
208         private final boolean mSingleEngine;
209 
WallpaperState( TestWallpaper homeWallpaper, TestWallpaper lockWallpaper, boolean singleEngine)210         WallpaperState(
211                 TestWallpaper homeWallpaper, TestWallpaper lockWallpaper, boolean singleEngine) {
212             mHomeWallpaper = homeWallpaper;
213             mLockWallpaper = lockWallpaper;
214             assertThat(!singleEngine || (homeWallpaper == lockWallpaper)).isTrue();
215             assertThat(homeWallpaper).isNotNull();
216             assertThat(lockWallpaper).isNotNull();
217             mSingleEngine = singleEngine;
218         }
219 
pickUnused(List<TestWallpaper> choices)220         private TestWallpaper pickUnused(List<TestWallpaper> choices) {
221             return choices.stream()
222                     .filter(wallpaper -> wallpaper != mHomeWallpaper && wallpaper != mLockWallpaper)
223                     .findFirst().orElseThrow();
224         }
225 
pickUnusedStatic()226         private TestWallpaper pickUnusedStatic() {
227             return pickUnused(allStaticTestWallpapers());
228         }
229 
pickUnusedLive()230         private TestWallpaper pickUnusedLive() {
231             return pickUnused(allLiveTestWallpapers());
232         }
233 
234         /**
235          * Enumerate all the possible logically different {@link WallpaperChange} changes from
236          * this state. <br>
237          * Two changes are considered logically different if their destination is different,
238          * or if their wallpaper type (static or live) is different.
239          */
allPossibleChanges()240         public List<WallpaperChange> allPossibleChanges() {
241             TestWallpaper unusedStatic = pickUnusedStatic();
242             TestWallpaper unusedLive = pickUnusedLive();
243 
244             // one can always add a new wallpaper, either static or live, at any destination
245             List<WallpaperChange> result = new ArrayList<>(Stream.of(unusedStatic, unusedLive)
246                     .flatMap(newWallpaper -> Stream
247                             .of(FLAG_LOCK, FLAG_SYSTEM, FLAG_LOCK | FLAG_SYSTEM)
248                             .map(destination -> new WallpaperChange(newWallpaper, destination)))
249                     .toList());
250 
251             // if we have a lock & home single engine, we can separate it
252             if (mSingleEngine) {
253                 result.addAll(List.of(
254                         new WallpaperChange(mHomeWallpaper, FLAG_SYSTEM),
255                         new WallpaperChange(mHomeWallpaper, FLAG_LOCK)
256                 ));
257 
258                 // else if we have the same engine twice, we can merge it
259             } else if (mHomeWallpaper == mLockWallpaper) {
260                 result.add(new WallpaperChange(mHomeWallpaper, FLAG_SYSTEM | FLAG_LOCK));
261             }
262 
263             // if we have different engines on home / lock,
264             // we can set one of them at the other location or at both locations
265             if (mHomeWallpaper != mLockWallpaper) {
266                 result.addAll(List.of(
267                         new WallpaperChange(mHomeWallpaper, FLAG_LOCK | FLAG_SYSTEM),
268                         new WallpaperChange(mLockWallpaper, FLAG_LOCK | FLAG_SYSTEM),
269                         new WallpaperChange(mHomeWallpaper, FLAG_LOCK),
270                         new WallpaperChange(mLockWallpaper, FLAG_SYSTEM)
271                 ));
272             }
273             return result;
274         }
275 
276         /**
277          * Given a change, return the number of times we expect an engine.onCreate operation
278          * of a live wallpaper from this state
279          */
expectedNumberOfLiveWallpaperCreate(WallpaperChange change)280         public int expectedNumberOfLiveWallpaperCreate(WallpaperChange change) {
281 
282             if (change.mWallpaper.isStatic()) return 0;
283             switch (change.mDestination) {
284                 case FLAG_SYSTEM | FLAG_LOCK:
285                     return change.mWallpaper != mHomeWallpaper ? 1 : 0;
286                 case FLAG_SYSTEM:
287                     return mSingleEngine || (change.mWallpaper != mHomeWallpaper) ? 1 : 0;
288                 case FLAG_LOCK:
289                     return mSingleEngine || (change.mWallpaper != mLockWallpaper) ? 1 : 0;
290                 default:
291                     throw new IllegalArgumentException();
292             }
293         }
294 
295 
296         /**
297          * Given a change, return the number of times we expect an engine.onDestroy operation
298          * of a live wallpaper from this state
299          */
expectedNumberOfLiveWallpaperDestroy(WallpaperChange change)300         public int expectedNumberOfLiveWallpaperDestroy(WallpaperChange change) {
301 
302             if (mSingleEngine) {
303                 return mHomeWallpaper.isLive()
304                         && mHomeWallpaper != change.mWallpaper
305                         && change.mDestination == (FLAG_LOCK | FLAG_SYSTEM) ? 1 : 0;
306             }
307 
308             boolean changeSystem = (change.mDestination & FLAG_SYSTEM) != 0;
309             boolean changeLock = (change.mDestination & FLAG_LOCK) != 0;
310             boolean systemReplaced = changeSystem && change.mWallpaper != mHomeWallpaper;
311             boolean lockReplaced =
312                     changeLock && (change.mWallpaper != mLockWallpaper || changeSystem);
313 
314             int result = 0;
315             if (systemReplaced && mHomeWallpaper.isLive()) result += 1;
316             if (lockReplaced && mLockWallpaper.isLive()) result += 1;
317             return result;
318         }
319 
320         /**
321          * Describes how to reproduce a failure obtained from this state with the given change
322          */
reproduceDescription(WallpaperChange change)323         public String reproduceDescription(WallpaperChange change) {
324             return String.format("To reproduce, start with:\n%s\nand %s",
325                     description(), changeDescription(change));
326         }
327 
description()328         private String description() {
329             String homeType = mHomeWallpaper.type();
330             String lockType = mLockWallpaper.type();
331             return mLockWallpaper == mHomeWallpaper
332                     ? String.format(" - the same %s wallpaper on home & lock screen (%s)", homeType,
333                     mSingleEngine ? "sharing the same engine" : "each using its own engine")
334                     : String.format(" - a %s wallpaper on home screen\n"
335                                     + " - %s %s wallpaper on lock screen",
336                             homeType, homeType.equals(lockType) ? "another" : "a", lockType);
337         }
338 
changeDescription(WallpaperChange change)339         private String changeDescription(WallpaperChange change) {
340             String newWallpaperDescription =
341                     change.mWallpaper == mHomeWallpaper || change.mWallpaper == mLockWallpaper
342                             ? String.format("the same %s wallpaper as %s screen",
343                             change.mWallpaper.type(),
344                             change.mWallpaper == mHomeWallpaper ? "home" : "lock")
345                             : String.format("a different %s wallpaper", change.mWallpaper.type());
346 
347             String destinationDescription =
348                     change.mDestination == FLAG_SYSTEM ? "home screen only"
349                             : change.mDestination == FLAG_LOCK ? "lock screen only"
350                                     : "both home & lock screens";
351 
352             String methodUsed = change.mWallpaper.isLive()
353                     ? "setWallpaperComponentWithFlags" : "setResource";
354 
355             String flagDescription =
356                     change.mDestination == FLAG_SYSTEM ? "FLAG_SYSTEM"
357                             : change.mDestination == FLAG_LOCK ? "FLAG_LOCK"
358                                     : "FLAG_SYSTEM|FLAG_LOCK";
359 
360             return String.format("apply %s on %s (via WallpaperManager#%s(..., %s))",
361                     newWallpaperDescription, destinationDescription, methodUsed, flagDescription);
362         }
363     }
364 
365     /**
366      * Uses the provided wallpaperManager instance to perform a {@link WallpaperChange}.
367      */
performChange( WallpaperManager wallpaperManager, WallpaperChange change)368     public static void performChange(
369             WallpaperManager wallpaperManager, WallpaperChange change)
370             throws IOException {
371         if (change.mWallpaper.isStatic()) {
372             wallpaperManager.setResource(
373                     change.mWallpaper.getBitmapResourceId(), change.mDestination);
374         } else {
375             // Up to one surface is expected to be created when switching wallpapers. It's possible
376             // that this operation ends up being a no-op, in that case the wait will time out.
377             final int expectedSurfaceCreations = 1;
378             runAndAwaitChanges(500, TimeUnit.MILLISECONDS, 0, 0, expectedSurfaceCreations,
379                     () -> {
380                         wallpaperManager.setWallpaperComponentWithFlags(
381                                 change.mWallpaper.getComponentName(), change.mDestination);
382                     });
383         }
384     }
385 
386     /**
387      * Sets a wallpaperManager in some state. Always proceeds the same way: <br>
388      *   - put the home wallpaper on lock and home screens <br>
389      *   - put the lock wallpaper on lock screen, if it is different from the home screen wallpaper
390      */
goToState( WallpaperManager wallpaperManager, WallpaperState state)391     public static void goToState(
392             WallpaperManager wallpaperManager, WallpaperState state)
393             throws IOException {
394         WallpaperChange change1 = new WallpaperChange(
395                 state.mHomeWallpaper, FLAG_SYSTEM | (state.mSingleEngine ? FLAG_LOCK : 0));
396         performChange(wallpaperManager, change1);
397 
398         WallpaperChange change2 = new WallpaperChange(state.mLockWallpaper, FLAG_LOCK);
399         if (!state.mSingleEngine) performChange(wallpaperManager, change2);
400     }
401 
402     static class ColorChangeWaiter implements WallpaperManager.OnColorsChangedListener {
403         private CountDownLatch mHomeCountDownLatch;
404         private CountDownLatch mLockCountDownLatch;
ColorChangeWaiter(int which)405         ColorChangeWaiter(int which) {
406             int expectedHomeEvents = ((which & FLAG_SYSTEM) != 0) ? 1 : 0;
407             int expectedLockEvents = ((which & FLAG_LOCK) != 0) ? 1 : 0;
408             mHomeCountDownLatch = new CountDownLatch(expectedHomeEvents);
409             mLockCountDownLatch = new CountDownLatch(expectedLockEvents);
410         }
411 
412         @Override
onColorsChanged(@ullable WallpaperColors colors, int which)413         public void onColorsChanged(@Nullable WallpaperColors colors, int which) {
414             if ((which & FLAG_SYSTEM) != 0) {
415                 mHomeCountDownLatch.countDown();
416             }
417             if ((which & FLAG_LOCK) != 0) {
418                 mLockCountDownLatch.countDown();
419             }
420             Log.d(TAG, "color state count down: " + which + " - " + colors);
421         }
422 
waitForChanges(long timeout, TimeUnit unit)423         public boolean waitForChanges(long timeout, TimeUnit unit) {
424             try {
425                 return mHomeCountDownLatch.await(timeout, unit)
426                         && mLockCountDownLatch.await(timeout, unit);
427             } catch (InterruptedException e) {
428                 throw new RuntimeException("Wallpaper colors wait interrupted");
429             }
430         }
431     }
432 }
433