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