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 android.app.Instrumentation;
20 import android.graphics.Rect;
21 import android.media.MediaMetadata;
22 import android.media.session.PlaybackState;
23 import android.platform.test.scenario.tapl_common.Gestures;
24 import android.util.Log;
25 
26 import androidx.test.platform.app.InstrumentationRegistry;
27 import androidx.test.uiautomator.By;
28 import androidx.test.uiautomator.BySelector;
29 import androidx.test.uiautomator.Direction;
30 import androidx.test.uiautomator.UiDevice;
31 import androidx.test.uiautomator.UiObject2;
32 import androidx.test.uiautomator.Until;
33 
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.concurrent.CountDownLatch;
37 import java.util.concurrent.TimeUnit;
38 
39 public class MediaController {
40     private static final String TAG = MediaController.class.getSimpleName();
41     private static final String PKG = "com.android.systemui";
42     private static final String HIDE_BTN_RES = "dismiss";
43     private static final BySelector PLAY_BTN_SELECTOR =
44         By.res(PKG, "actionPlayPause").descContains("Play");
45     private static final BySelector PAUSE_BTN_SELECTOR =
46         By.res(PKG, "actionPlayPause").descContains("Pause");
47     private static final BySelector SKIP_NEXT_BTN_SELECTOR =
48         By.res(PKG, "actionNext").descContains("Next");
49     private static final BySelector SKIP_PREV_BTN_SELECTOR =
50         By.res(PKG, "actionPrev").descContains("Previous");
51     private static final int WAIT_TIME_MILLIS = 10_000;
52     private static final int LONG_PRESS_TIME_MILLIS = 1_000;
53     private static final long UI_WAIT_TIMEOUT = 3_000;
54 
55     private final UiObject2 mUiObject;
56     private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
57     private final UiDevice mDevice = UiDevice.getInstance(mInstrumentation);
58     private final List<Integer> mStateChanges;
59     private Runnable mStateListener;
60     private static final Object sStateListenerLock = new Object();
61 
MediaController(MediaInstrumentation media, UiObject2 uiObject)62     MediaController(MediaInstrumentation media, UiObject2 uiObject) {
63         media.addMediaSessionStateChangedListeners(this::onMediaSessionStageChanged);
64         mUiObject = uiObject;
65         mStateChanges = new ArrayList<>();
66     }
67 
play()68     public void play() {
69         runToNextState(
70                 () -> {
71                     mInstrumentation.getUiAutomation().clearCache();
72                     mUiObject.wait(Until.findObject(PLAY_BTN_SELECTOR), WAIT_TIME_MILLIS).click();
73                 },
74                 PlaybackState.STATE_PLAYING);
75     }
76 
pause()77     public void pause() {
78         runToNextState(
79                 () -> {
80                     mInstrumentation.getUiAutomation().clearCache();
81                     Gestures.click(
82                             mUiObject.wait(Until.findObject(PAUSE_BTN_SELECTOR), WAIT_TIME_MILLIS),
83                             "Pause button");
84                 },
85                 PlaybackState.STATE_PAUSED);
86     }
87 
skipToNext()88     public void skipToNext() {
89         runToNextState(
90                 () -> Gestures.click(
91                         mUiObject.wait(Until.findObject(SKIP_NEXT_BTN_SELECTOR), WAIT_TIME_MILLIS),
92                         "Next button"),
93                 PlaybackState.STATE_SKIPPING_TO_NEXT);
94     }
95 
skipToPrev()96     public void skipToPrev() {
97         runToNextState(
98                 () -> Gestures.click(
99                         mUiObject.wait(Until.findObject(SKIP_PREV_BTN_SELECTOR), WAIT_TIME_MILLIS),
100                         "Previous button"),
101                 PlaybackState.STATE_SKIPPING_TO_PREVIOUS);
102     }
103 
runToNextState(Runnable runnable, int state)104     private void runToNextState(Runnable runnable, int state) {
105         mStateChanges.clear();
106         CountDownLatch latch = new CountDownLatch(1);
107         synchronized (sStateListenerLock) {
108             mStateListener = latch::countDown;
109         }
110         runnable.run();
111         try {
112             if (!latch.await(WAIT_TIME_MILLIS, TimeUnit.MILLISECONDS)) {
113                 throw new RuntimeException(
114                         "PlaybackState didn't change to state:" + state + " and timeout.");
115             }
116         } catch (InterruptedException e) {
117             throw new RuntimeException();
118         }
119         if (!mStateChanges.contains(state)) {
120             throw new RuntimeException(String.format("Fail to run to next state(%d).", state));
121         }
122     }
123 
onMediaSessionStageChanged(int state)124     private void onMediaSessionStageChanged(int state) {
125         mStateChanges.add(state);
126         if (mStateListener != null) {
127             synchronized (sStateListenerLock) {
128                 mStateListener.run();
129                 mStateListener = null;
130             }
131         }
132     }
133 
title()134     public String title() {
135         UiObject2 header =
136             mUiObject.wait(Until.findObject(By.res(PKG, "header_title")), WAIT_TIME_MILLIS);
137         if (header == null) {
138             return "";
139         }
140         return header.getText();
141     }
142 
143     /**
144      * Long press for {@link #LONG_PRESS_TIME_MILLIS} ms on UMO then clik the hide button.
145      */
longPressAndHide()146     public void longPressAndHide() {
147         if (mUiObject == null) {
148             throw new RuntimeException("UMO should exist to do long press.");
149         }
150 
151         mUiObject.click(LONG_PRESS_TIME_MILLIS);
152         UiObject2 hideBtn = mUiObject.wait(
153             Until.findObject(By.res(PKG, HIDE_BTN_RES)), WAIT_TIME_MILLIS);
154         if (hideBtn == null) {
155             throw new RuntimeException("Hide button should exist after long press on UMO.");
156         }
157         hideBtn.clickAndWait(Until.newWindow(), UI_WAIT_TIMEOUT);
158     }
159 
160     /**
161      * Checks if the current media session is using the given MediaMetadata.
162      *
163      * @param meta MediaMetadata to get media title and artist.
164      * @return boolean
165      */
hasMetadata(MediaMetadata meta)166     public boolean hasMetadata(MediaMetadata meta) {
167         Log.d(
168                 TAG,
169                 "[Check metadata] hasMetadata: By.header_title="
170                         + meta.getString(MediaMetadata.METADATA_KEY_TITLE)
171                         + "By.header_artist="
172                         + meta.getString(MediaMetadata.METADATA_KEY_ARTIST));
173         final BySelector mediaTitleSelector =
174             By.res(PKG, "header_title").text(meta.getString(MediaMetadata.METADATA_KEY_TITLE));
175         final BySelector mediaArtistSelector =
176             By.res(PKG, "header_artist")
177                 .text(meta.getString(MediaMetadata.METADATA_KEY_ARTIST));
178         mInstrumentation.getUiAutomation().clearCache();
179         final boolean titleCheckResult = mUiObject.hasObject(mediaTitleSelector);
180         final boolean artistCheckResult = mUiObject.hasObject(mediaArtistSelector);
181 
182         Log.d(
183                 TAG,
184                 "[Check metadata] title: " + titleCheckResult + ". artist: " + artistCheckResult);
185         return titleCheckResult && artistCheckResult;
186     }
187 
swipe(Direction direction)188     public boolean swipe(Direction direction) {
189         Rect bound = mUiObject.getVisibleBounds();
190         final int startX;
191         final int endX;
192         switch (direction) {
193             case LEFT:
194                 startX = (bound.right + bound.centerX()) / 2;
195                 endX = bound.left;
196                 break;
197             case RIGHT:
198                 startX = (bound.left + bound.centerX()) / 2;
199                 endX = bound.right;
200                 break;
201             default:
202                 throw new RuntimeException(
203                     String.format("swipe to %s on UMO isn't supported.", direction));
204         }
205         return mDevice.swipe(startX, bound.centerY(), endX, bound.centerY(), 10);
206     }
207 
getVisibleBound()208     public Rect getVisibleBound() {
209         return mUiObject.getVisibleBounds();
210     }
211 }
212