• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.platform.helpers.media;
18 
19 import static android.platform.uiautomatorhelpers.DeviceHelpers.assertVisibility;
20 
21 import static org.junit.Assert.assertNotNull;
22 
23 import android.app.Notification;
24 import android.app.NotificationChannel;
25 import android.app.NotificationManager;
26 import android.content.Context;
27 import android.graphics.Rect;
28 import android.media.MediaMetadata;
29 import android.media.session.MediaSession;
30 import android.media.session.PlaybackState;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.platform.test.util.HealthTestingUtils;
34 
35 import androidx.test.platform.app.InstrumentationRegistry;
36 import androidx.test.uiautomator.By;
37 import androidx.test.uiautomator.BySelector;
38 import androidx.test.uiautomator.Direction;
39 import androidx.test.uiautomator.UiDevice;
40 import androidx.test.uiautomator.UiObject2;
41 import androidx.test.uiautomator.Until;
42 
43 import java.time.Duration;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.function.Consumer;
47 
48 /** Media instrumentation for testing. */
49 public final class MediaInstrumentation {
50 
51     private static final int WAIT_TIME_MILLIS = 5000;
52     private static final String PKG = "com.android.systemui";
53     private static final String MEDIA_CONTROLLER_RES_ID = "qs_media_controls";
54     private static int notificationID = 0;
55 
56     private final UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
57 
58     private final String mChannelId;
59     private final NotificationManager mManager;
60     private final MediaSession mMediaSession;
61     private final Handler mHandler;
62     private final MediaSessionCallback mCallback;
63     private final Context mContext;
64     // TODO(bennolin): support legacy version media controller. Please refer
65     //  go/media-t-app-changes for more details.
66     private final boolean mUseLegacyVersion;
67     private final List<Consumer<Integer>> mMediaSessionStateChangedListeners;
68     private final int mNotificationId;
69     private final MockMediaPlayer mPlayer;
70     private int mCurrentMediaState;
71 
72     // the idx of mMediaSources which represents current media source.
73     private int mCurrentMediaSource;
74     private final List<MediaMetadata> mMediaSources;
75 
MediaInstrumentation( Context context, MediaSession mediaSession, List<MediaMetadata> mediaSources, String channelId, boolean useLegacyVersion )76     private MediaInstrumentation(
77             Context context, MediaSession mediaSession,
78             List<MediaMetadata> mediaSources,
79             String channelId, boolean useLegacyVersion
80     ) {
81         mHandler = new Handler(Looper.getMainLooper());
82         mContext = context;
83         mMediaSession = mediaSession;
84         mChannelId = channelId;
85         mUseLegacyVersion = useLegacyVersion;
86         mManager = context.getSystemService(NotificationManager.class);
87         mCurrentMediaState = PlaybackState.STATE_NONE;
88         mPlayer = new MockMediaPlayer();
89         mCallback = new MediaSessionCallback(mPlayer);
90         mMediaSources = mediaSources;
91         mCurrentMediaSource = 0;
92         mNotificationId = ++notificationID;
93         mMediaSessionStateChangedListeners = new ArrayList<>();
94         initialize();
95     }
96 
initialize()97     private void initialize() {
98         mHandler.post(() -> mMediaSession.setCallback(mCallback));
99         mCallback.addOnMediaStateChangedListener(this::onMediaSessionStateChanged);
100         mCallback.addOnMediaStateChangedListener(this::onMediaSessionSkipTo);
101         MediaMetadata source = mMediaSources.stream().findFirst().orElse(null);
102         mMediaSession.setMetadata(source);
103         mMediaSession.setActive(true);
104         mPlayer.setDataSource(source);
105         mPlayer.setOnCompletionListener(() -> setCurrentMediaState(PlaybackState.STATE_STOPPED));
106         setCurrentMediaState(
107                 source == null ? PlaybackState.STATE_NONE : PlaybackState.STATE_STOPPED);
108     }
109 
buildNotification()110     Notification.Builder buildNotification() {
111         return new Notification.Builder(mContext, mChannelId)
112                 .setContentTitle("MediaInstrumentation")
113                 .setContentText("media")
114                 .setSmallIcon(android.R.drawable.stat_sys_headset)
115                 .setStyle(new Notification.MediaStyle()
116                         .setMediaSession(mMediaSession.getSessionToken()));
117     }
118 
createNotification()119     public void createNotification() {
120         mManager.notify(mNotificationId, buildNotification().build());
121     }
122 
123     /** Cancel the Media notification */
cancelNotification()124     public void cancelNotification() {
125         mManager.cancel(mNotificationId);
126     }
127 
scrollToMediaNotification(MediaMetadata meta)128     UiObject2 scrollToMediaNotification(MediaMetadata meta) {
129         final BySelector qsScrollViewSelector = By.res(PKG, "expanded_qs_scroll_view");
130         final BySelector mediaTitleSelector = By.res(PKG, "header_title")
131                 .text(meta.getString(MediaMetadata.METADATA_KEY_TITLE));
132         final BySelector umoSelector = By.res(PKG, MEDIA_CONTROLLER_RES_ID)
133                 .hasDescendant(mediaTitleSelector);
134         UiObject2 notification = mDevice.wait(Until.findObject(umoSelector), WAIT_TIME_MILLIS);
135         if (notification == null) {
136             // Try to scroll down the QS container to make UMO visible.
137             UiObject2 qsScrollView = mDevice.wait(Until.findObject(qsScrollViewSelector),
138                     WAIT_TIME_MILLIS);
139             assertNotNull("Unable to scroll the QS container.", qsScrollView);
140             qsScrollView.scroll(Direction.DOWN, 1.0f, 100);
141             InstrumentationRegistry.getInstrumentation().getUiAutomation().clearCache();
142             notification = mDevice.wait(Until.findObject(umoSelector), WAIT_TIME_MILLIS);
143         }
144         assertNotNull("Unable to find UMO.", notification);
145         // The UMO may still not be fully visible, double check it's visibility.
146         notification = ensureUMOFullyVisible(notification);
147         assertNotNull("UMO isn't fully visible.", notification);
148         mDevice.waitForIdle();
149         HealthTestingUtils.waitForValueToSettle(
150                 () -> "UMO isn't settle after timeout.", notification::getVisibleBounds);
151         return notification;
152     }
153 
ensureUMOFullyVisible(UiObject2 umo)154     private UiObject2 ensureUMOFullyVisible(UiObject2 umo) {
155         final BySelector footerSelector = By.res(PKG, "qs_footer_actions");
156         UiObject2 footer = mDevice.wait(Until.findObject(footerSelector), WAIT_TIME_MILLIS);
157         assertNotNull("Can't find QS actions footer.", footer);
158         Rect umoBound = umo.getVisibleBounds();
159         Rect footerBound = footer.getVisibleBounds();
160         int distance = umoBound.bottom - footerBound.top;
161         if (distance <= 0) {
162             return umo;
163         }
164         distance += footerBound.height();
165         UiObject2 scrollable = mDevice.wait(Until.findObject(By.scrollable(true)), WAIT_TIME_MILLIS);
166         scrollable.scroll(
167                 Direction.DOWN, (float)distance / scrollable.getVisibleBounds().height(), 100);
168         InstrumentationRegistry.getInstrumentation().getUiAutomation().clearCache();
169         return mDevice.wait(Until.findObject(By.res(umo.getResourceName())), WAIT_TIME_MILLIS);
170     }
171 
172     /**
173      * Find the UMO that belongs to the current MediaInstrumentation (Media Session).
174      * If the UMO can't be found, the function will raise an assertion error.
175      *
176      * @return MediaController
177      */
getMediaNotification()178     public MediaController getMediaNotification() {
179         MediaMetadata source = mMediaSources.stream().findFirst().orElseThrow();
180         UiObject2 notification = scrollToMediaNotification(source);
181         return new MediaController(this, notification);
182     }
183 
184     /**
185      * Find the UMO in current view. This method will only check UMO in current view page different
186      * than {@link #getMediaNotification()} to seek UMO in quick setting view.
187      *
188      * @return MediaController
189      * @throws AssertionError if the UMO can't be found in current view.
190      */
getMediaNotificationInCurrentView()191     public MediaController getMediaNotificationInCurrentView() {
192         MediaMetadata source = mMediaSources.stream().findFirst().orElseThrow();
193         final BySelector mediaTitleSelector = By.res(PKG, "header_title")
194                 .text(source.getString(MediaMetadata.METADATA_KEY_TITLE));
195         final BySelector umoSelector = By.res(PKG, MEDIA_CONTROLLER_RES_ID)
196                 .hasDescendant(mediaTitleSelector);
197         UiObject2 notification = mDevice.wait(Until.findObject(umoSelector), WAIT_TIME_MILLIS);
198         assertNotNull("Unable to find UMO.", notification);
199         mDevice.waitForIdle();
200         HealthTestingUtils.waitForValueToSettle(
201                 () -> "UMO isn't settle after timeout.", notification::getVisibleBounds);
202         return new MediaController(this, notification);
203     }
204 
205     /**
206      * Wait for UMO is gone.
207      *
208      * @param timeout Maximum amount of time to wait in milliseconds.
209      * @return The final result returned by the condition, or null if the condition was not met
210      *     before the timeout.
211      */
waitUmoGone(long timeout)212     public boolean waitUmoGone(long timeout) {
213         return mDevice.wait(Until.gone(By.res(PKG, MEDIA_CONTROLLER_RES_ID)), timeout);
214     }
215 
isMediaNotificationVisible()216     public boolean isMediaNotificationVisible() {
217         return mDevice.hasObject(By.res(PKG, MEDIA_CONTROLLER_RES_ID));
218     }
219 
220     /** Assert that the media notification is visible with a 10 second timeout. */
assertMediaNotificationVisible()221     public void assertMediaNotificationVisible() {
222         assertVisibility(
223                 By.res(PKG, MEDIA_CONTROLLER_RES_ID),
224                 true,
225                 Duration.ofSeconds(10),
226                 () -> "UMO should be visible on lockscreen.");
227     }
228 
addMediaSessionStateChangedListeners(Consumer<Integer> listener)229     public void addMediaSessionStateChangedListeners(Consumer<Integer> listener) {
230         mMediaSessionStateChangedListeners.add(listener);
231     }
232 
clearMediaSessionStateChangedListeners()233     public void clearMediaSessionStateChangedListeners() {
234         mMediaSessionStateChangedListeners.clear();
235     }
236 
onMediaSessionStateChanged(int state)237     private void onMediaSessionStateChanged(int state) {
238         setCurrentMediaState(state);
239         for (Consumer<Integer> listener : mMediaSessionStateChangedListeners) {
240             listener.accept(state);
241         }
242     }
243 
onMediaSessionSkipTo(int state)244     private void onMediaSessionSkipTo(int state) {
245         final int sources = mMediaSources.size();
246         if (sources <= 0) { // no media sources to skip to
247             return;
248         }
249         switch (state) {
250             case PlaybackState.STATE_SKIPPING_TO_NEXT:
251                 mCurrentMediaSource = (mCurrentMediaSource + 1) % sources;
252                 break;
253             case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
254                 mCurrentMediaSource = (mCurrentMediaSource - 1) % sources;
255                 break;
256             default: // the state changing isn't related to skip.
257                 return;
258         }
259         mMediaSession.setMetadata(mMediaSources.get(mCurrentMediaSource));
260         mPlayer.setDataSource(mMediaSources.get(mCurrentMediaSource));
261         mPlayer.reset();
262         mPlayer.pause();
263         setCurrentMediaState(PlaybackState.STATE_PAUSED);
264         createNotification();
265     }
266 
updatePlaybackState()267     private void updatePlaybackState() {
268         if (mUseLegacyVersion) {
269             // TODO(bennolin): add legacy version, be aware of `setState`  and  `ACTION_SEEK_TO`
270             //  are still relevant to legacy version controller.
271             return;
272         }
273         mMediaSession.setPlaybackState(new PlaybackState.Builder()
274                 .setActions(getAvailableActions(mCurrentMediaState))
275                 .setState(mCurrentMediaState, mPlayer.getCurrentPosition(), 1.0f)
276                 .build());
277     }
278 
279     /**
280      * Sets the Media's state to the given state.
281      *
282      * @param state the {@link PlaybackState}.
283      */
setCurrentMediaState(int state)284     public void setCurrentMediaState(int state) {
285         mCurrentMediaState = state;
286         updatePlaybackState();
287     }
288 
getAvailableActions(int state)289     private Long getAvailableActions(int state) {
290         switch (state) {
291             case PlaybackState.STATE_PLAYING:
292                 return PlaybackState.ACTION_PAUSE
293                         | PlaybackState.ACTION_SEEK_TO
294                         | PlaybackState.ACTION_SKIP_TO_NEXT
295                         | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
296             case PlaybackState.STATE_PAUSED:
297                 return PlaybackState.ACTION_PLAY
298                         | PlaybackState.ACTION_STOP
299                         | PlaybackState.ACTION_SKIP_TO_NEXT
300                         | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
301             case PlaybackState.STATE_STOPPED:
302                 return PlaybackState.ACTION_PLAY
303                         | PlaybackState.ACTION_PAUSE
304                         | PlaybackState.ACTION_SKIP_TO_NEXT
305                         | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
306             default:
307                 return PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE
308                         | PlaybackState.ACTION_STOP | PlaybackState.ACTION_SEEK_TO;
309         }
310     }
311 
312     public static class Builder {
313 
314         private final boolean mUseLegacyVersion;
315         private final Context mContext;
316         private final MediaSession mSession;
317         private String mChannelId;
318         private final List<MediaMetadata> mDataSources;
319 
Builder(Context context, MediaSession session)320         public Builder(Context context, MediaSession session) {
321             mUseLegacyVersion = false;
322             mContext = context;
323             mChannelId = "";
324             mSession = session;
325             mDataSources = new ArrayList<>();
326         }
327 
setChannelId(String id)328         public Builder setChannelId(String id) {
329             mChannelId = id;
330             return this;
331         }
332 
addDataSource(MediaMetadata source)333         public Builder addDataSource(MediaMetadata source) {
334             mDataSources.add(source);
335             return this;
336         }
337 
build()338         public MediaInstrumentation build() {
339             if (mChannelId.isEmpty()) {
340                 NotificationManager manager = mContext.getSystemService(NotificationManager.class);
341                 mChannelId = MediaInstrumentation.class.getCanonicalName();
342                 NotificationChannel channel = new NotificationChannel(
343                         mChannelId, "Default", NotificationManager.IMPORTANCE_DEFAULT);
344                 manager.createNotificationChannel(channel);
345             }
346             return new MediaInstrumentation(
347                     mContext, mSession, mDataSources, mChannelId, mUseLegacyVersion);
348         }
349     }
350 }
351