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