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