1 /* 2 * Copyright (C) 2020 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 com.android.wm.shell.flicker.helpers 18 19 import android.app.Instrumentation 20 import android.graphics.Rect 21 import android.media.session.MediaController 22 import android.media.session.MediaSessionManager 23 import android.os.SystemClock 24 import androidx.test.uiautomator.By 25 import androidx.test.uiautomator.BySelector 26 import androidx.test.uiautomator.Until 27 import com.android.server.wm.flicker.helpers.FIND_TIMEOUT 28 import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE 29 import com.android.server.wm.traces.parser.toFlickerComponent 30 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper 31 import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow 32 import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild 33 import com.android.wm.shell.flicker.testapp.Components 34 35 class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( 36 instrumentation, 37 Components.PipActivity.LABEL, 38 Components.PipActivity.COMPONENT.toFlickerComponent() 39 ) { 40 private val mediaSessionManager: MediaSessionManager 41 get() = context.getSystemService(MediaSessionManager::class.java) 42 ?: error("Could not get MediaSessionManager") 43 44 private val mediaController: MediaController? 45 get() = mediaSessionManager.getActiveSessions(null).firstOrNull { 46 it.packageName == component.packageName 47 } 48 49 fun clickObject(resId: String) { 50 val selector = By.res(component.packageName, resId) 51 val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") 52 53 if (!isTelevision) { 54 obj.click() 55 } else { 56 focusOnObject(selector) || error("Could not focus on `$resId` object") 57 uiDevice.pressDPadCenter() 58 } 59 } 60 61 /** {@inheritDoc} */ 62 override fun launchViaIntent( 63 wmHelper: WindowManagerStateHelper, 64 expectedWindowName: String, 65 action: String?, 66 stringExtras: Map<String, String> 67 ) { 68 super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras) 69 wmHelper.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } 70 } 71 72 private fun focusOnObject(selector: BySelector): Boolean { 73 // We expect all the focusable UI elements to be arranged in a way so that it is possible 74 // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top" 75 // from "the bottom". 76 repeat(FOCUS_ATTEMPTS) { 77 uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } 78 ?: error("The object we try to focus on is gone.") 79 80 uiDevice.pressDPadDown() 81 uiDevice.waitForIdle() 82 } 83 return false 84 } 85 86 @JvmOverloads 87 fun clickEnterPipButton(wmHelper: WindowManagerStateHelper? = null) { 88 clickObject(ENTER_PIP_BUTTON_ID) 89 90 // Wait on WMHelper or simply wait for 3 seconds 91 wmHelper?.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000) 92 // when entering pip, the dismiss button is visible at the start. to ensure the pip 93 // animation is complete, wait until the pip dismiss button is no longer visible. 94 // b/176822698: dismiss-only state will be removed in the future 95 uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT) 96 } 97 98 fun clickStartMediaSessionButton() { 99 clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID) 100 } 101 102 fun checkWithCustomActionsCheckbox() = uiDevice 103 .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) 104 ?.takeIf { it.isCheckable } 105 ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } 106 ?: error("'With custom actions' checkbox not found") 107 108 fun pauseMedia() = mediaController?.transportControls?.pause() 109 ?: error("No active media session found") 110 111 fun stopMedia() = mediaController?.transportControls?.stop() 112 ?: error("No active media session found") 113 114 @Deprecated("Use PipAppHelper.closePipWindow(wmHelper) instead", 115 ReplaceWith("closePipWindow(wmHelper)")) 116 fun closePipWindow() { 117 if (isTelevision) { 118 uiDevice.closeTvPipWindow() 119 } else { 120 closePipWindow(WindowManagerStateHelper(mInstrumentation)) 121 } 122 } 123 124 private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect { 125 val windowRegion = wmHelper.getWindowRegion(component) 126 require(!windowRegion.isEmpty) { 127 "Unable to find a PIP window in the current state" 128 } 129 return windowRegion.bounds 130 } 131 132 /** 133 * Taps the pip window and dismisses it by clicking on the X button. 134 */ 135 fun closePipWindow(wmHelper: WindowManagerStateHelper) { 136 if (isTelevision) { 137 uiDevice.closeTvPipWindow() 138 } else { 139 val windowRect = getWindowRect(wmHelper) 140 uiDevice.click(windowRect.centerX(), windowRect.centerY()) 141 // search and interact with the dismiss button 142 val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") 143 uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) 144 val dismissPipObject = uiDevice.findObject(dismissSelector) 145 ?: error("PIP window dismiss button not found") 146 val dismissButtonBounds = dismissPipObject.visibleBounds 147 uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) 148 } 149 150 // Wait for animation to complete. 151 wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } 152 wmHelper.waitForHomeActivityVisible() 153 } 154 155 /** 156 * Close the pip window by pressing the expand button 157 */ 158 fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { 159 val windowRect = getWindowRect(wmHelper) 160 uiDevice.click(windowRect.centerX(), windowRect.centerY()) 161 // search and interact with the expand button 162 val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button") 163 uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT) 164 val expandPipObject = uiDevice.findObject(expandSelector) 165 ?: error("PIP window expand button not found") 166 val expandButtonBounds = expandPipObject.visibleBounds 167 uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) 168 wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } 169 wmHelper.waitForAppTransitionIdle() 170 } 171 172 /** 173 * Double click on the PIP window to expand it 174 */ 175 fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) { 176 val windowRect = getWindowRect(wmHelper) 177 uiDevice.click(windowRect.centerX(), windowRect.centerY()) 178 uiDevice.click(windowRect.centerX(), windowRect.centerY()) 179 wmHelper.waitForAppTransitionIdle() 180 } 181 182 companion object { 183 private const val FOCUS_ATTEMPTS = 20 184 private const val ENTER_PIP_BUTTON_ID = "enter_pip" 185 private const val WITH_CUSTOM_ACTIONS_BUTTON_ID = "with_custom_actions" 186 private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start" 187 } 188 } 189