• Home
  • History
  • Annotate
  • 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.uiautomator_helpers.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              notification = mDevice.wait(Until.findObject(umoSelector), WAIT_TIME_MILLIS);
142          }
143          assertNotNull("Unable to find UMO.", notification);
144          // The UMO may still not be fully visible, double check it's visibility.
145          notification = ensureUMOFullyVisible(notification);
146          assertNotNull("UMO isn't fully visible.", notification);
147          mDevice.waitForIdle();
148          HealthTestingUtils.waitForValueToSettle(
149                  () -> "UMO isn't settle after timeout.", notification::getVisibleBounds);
150          return notification;
151      }
152  
ensureUMOFullyVisible(UiObject2 umo)153      private UiObject2 ensureUMOFullyVisible(UiObject2 umo) {
154          final BySelector footerSelector = By.res(PKG, "qs_footer_actions");
155          UiObject2 footer = mDevice.wait(Until.findObject(footerSelector), WAIT_TIME_MILLIS);
156          assertNotNull("Can't find QS actions footer.", footer);
157          Rect umoBound = umo.getVisibleBounds();
158          Rect footerBound = footer.getVisibleBounds();
159          int distance = umoBound.bottom - footerBound.top;
160          if (distance <= 0) {
161              return umo;
162          }
163          distance += footerBound.height();
164          UiObject2 scrollable = mDevice.wait(Until.findObject(By.scrollable(true)), WAIT_TIME_MILLIS);
165          scrollable.scroll(
166                  Direction.DOWN, (float)distance / scrollable.getVisibleBounds().height(), 100);
167          return mDevice.wait(Until.findObject(By.res(umo.getResourceName())), WAIT_TIME_MILLIS);
168      }
169  
170      /**
171       * Find the UMO that belongs to the current MediaInstrumentation (Media Session).
172       * If the UMO can't be found, the function will raise an assertion error.
173       *
174       * @return MediaController
175       */
getMediaNotification()176      public MediaController getMediaNotification() {
177          MediaMetadata source = mMediaSources.stream().findFirst().orElseThrow();
178          UiObject2 notification = scrollToMediaNotification(source);
179          return new MediaController(this, notification);
180      }
181  
182      /**
183       * Find the UMO in current view. This method will only check UMO in current view page different
184       * than {@link #getMediaNotification()} to seek UMO in quick setting view.
185       *
186       * @return MediaController
187       * @throws AssertionError if the UMO can't be found in current view.
188       */
getMediaNotificationInCurrentView()189      public MediaController getMediaNotificationInCurrentView() {
190          MediaMetadata source = mMediaSources.stream().findFirst().orElseThrow();
191          final BySelector mediaTitleSelector = By.res(PKG, "header_title")
192                  .text(source.getString(MediaMetadata.METADATA_KEY_TITLE));
193          final BySelector umoSelector = By.res(PKG, MEDIA_CONTROLLER_RES_ID)
194                  .hasDescendant(mediaTitleSelector);
195          UiObject2 notification = mDevice.wait(Until.findObject(umoSelector), WAIT_TIME_MILLIS);
196          assertNotNull("Unable to find UMO.", notification);
197          mDevice.waitForIdle();
198          HealthTestingUtils.waitForValueToSettle(
199                  () -> "UMO isn't settle after timeout.", notification::getVisibleBounds);
200          return new MediaController(this, notification);
201      }
202  
203      /**
204       * Wait for UMO is gone.
205       *
206       * @param timeout Maximum amount of time to wait in milliseconds.
207       * @return The final result returned by the condition, or null if the condition was not met
208       *     before the timeout.
209       */
waitUmoGone(long timeout)210      public boolean waitUmoGone(long timeout) {
211          return mDevice.wait(Until.gone(By.res(PKG, MEDIA_CONTROLLER_RES_ID)), timeout);
212      }
213  
isMediaNotificationVisible()214      public boolean isMediaNotificationVisible() {
215          return mDevice.hasObject(By.res(PKG, MEDIA_CONTROLLER_RES_ID));
216      }
217  
218      /** Assert that the media notification is visible with a 10 second timeout. */
assertMediaNotificationVisible()219      public void assertMediaNotificationVisible() {
220          assertVisibility(
221                  By.res(PKG, MEDIA_CONTROLLER_RES_ID),
222                  true,
223                  Duration.ofSeconds(10),
224                  () -> "UMO should be visible on lockscreen.");
225      }
226  
addMediaSessionStateChangedListeners(Consumer<Integer> listener)227      public void addMediaSessionStateChangedListeners(Consumer<Integer> listener) {
228          mMediaSessionStateChangedListeners.add(listener);
229      }
230  
clearMediaSessionStateChangedListeners()231      public void clearMediaSessionStateChangedListeners() {
232          mMediaSessionStateChangedListeners.clear();
233      }
234  
onMediaSessionStateChanged(int state)235      private void onMediaSessionStateChanged(int state) {
236          setCurrentMediaState(state);
237          for (Consumer<Integer> listener : mMediaSessionStateChangedListeners) {
238              listener.accept(state);
239          }
240      }
241  
onMediaSessionSkipTo(int state)242      private void onMediaSessionSkipTo(int state) {
243          final int sources = mMediaSources.size();
244          if (sources <= 0) { // no media sources to skip to
245              return;
246          }
247          switch (state) {
248              case PlaybackState.STATE_SKIPPING_TO_NEXT:
249                  mCurrentMediaSource = (mCurrentMediaSource + 1) % sources;
250                  break;
251              case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
252                  mCurrentMediaSource = (mCurrentMediaSource - 1) % sources;
253                  break;
254              default: // the state changing isn't related to skip.
255                  return;
256          }
257          mMediaSession.setMetadata(mMediaSources.get(mCurrentMediaSource));
258          mPlayer.setDataSource(mMediaSources.get(mCurrentMediaSource));
259          mPlayer.reset();
260          mPlayer.pause();
261          setCurrentMediaState(PlaybackState.STATE_PAUSED);
262          createNotification();
263      }
264  
updatePlaybackState()265      private void updatePlaybackState() {
266          if (mUseLegacyVersion) {
267              // TODO(bennolin): add legacy version, be aware of `setState`  and  `ACTION_SEEK_TO`
268              //  are still relevant to legacy version controller.
269              return;
270          }
271          mMediaSession.setPlaybackState(new PlaybackState.Builder()
272                  .setActions(getAvailableActions(mCurrentMediaState))
273                  .setState(mCurrentMediaState, mPlayer.getCurrentPosition(), 1.0f)
274                  .build());
275      }
276  
277      /**
278       * Sets the Media's state to the given state.
279       *
280       * @param state the {@link PlaybackState}.
281       */
setCurrentMediaState(int state)282      public void setCurrentMediaState(int state) {
283          mCurrentMediaState = state;
284          updatePlaybackState();
285      }
286  
getAvailableActions(int state)287      private Long getAvailableActions(int state) {
288          switch (state) {
289              case PlaybackState.STATE_PLAYING:
290                  return PlaybackState.ACTION_PAUSE
291                          | PlaybackState.ACTION_SEEK_TO
292                          | PlaybackState.ACTION_SKIP_TO_NEXT
293                          | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
294              case PlaybackState.STATE_PAUSED:
295                  return PlaybackState.ACTION_PLAY
296                          | PlaybackState.ACTION_STOP
297                          | PlaybackState.ACTION_SKIP_TO_NEXT
298                          | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
299              case PlaybackState.STATE_STOPPED:
300                  return PlaybackState.ACTION_PLAY
301                          | PlaybackState.ACTION_PAUSE
302                          | PlaybackState.ACTION_SKIP_TO_NEXT
303                          | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
304              default:
305                  return PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE
306                          | PlaybackState.ACTION_STOP | PlaybackState.ACTION_SEEK_TO;
307          }
308      }
309  
310      public static class Builder {
311  
312          private final boolean mUseLegacyVersion;
313          private final Context mContext;
314          private final MediaSession mSession;
315          private String mChannelId;
316          private final List<MediaMetadata> mDataSources;
317  
Builder(Context context, MediaSession session)318          public Builder(Context context, MediaSession session) {
319              mUseLegacyVersion = false;
320              mContext = context;
321              mChannelId = "";
322              mSession = session;
323              mDataSources = new ArrayList<>();
324          }
325  
setChannelId(String id)326          public Builder setChannelId(String id) {
327              mChannelId = id;
328              return this;
329          }
330  
addDataSource(MediaMetadata source)331          public Builder addDataSource(MediaMetadata source) {
332              mDataSources.add(source);
333              return this;
334          }
335  
build()336          public MediaInstrumentation build() {
337              if (mChannelId.isEmpty()) {
338                  NotificationManager manager = mContext.getSystemService(NotificationManager.class);
339                  mChannelId = MediaInstrumentation.class.getCanonicalName();
340                  NotificationChannel channel = new NotificationChannel(
341                          mChannelId, "Default", NotificationManager.IMPORTANCE_DEFAULT);
342                  manager.createNotificationChannel(channel);
343              }
344              return new MediaInstrumentation(
345                      mContext, mSession, mDataSources, mChannelId, mUseLegacyVersion);
346          }
347      }
348  }
349