blob: 19ee09a81ec53d891e4c6a1066c49f912cdce4c5 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.wm.flicker.helpers
import android.app.Instrumentation
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.util.Log
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import com.android.server.wm.flicker.helpers.GestureHelper.Tuple
import com.android.server.wm.flicker.testapp.ActivityOptions
import com.android.server.wm.traces.common.Rect
import com.android.server.wm.traces.common.WindowManagerConditionsFactory
import com.android.server.wm.traces.common.region.Region
import com.android.server.wm.traces.parser.toFlickerComponent
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
open class PipAppHelper(instrumentation: Instrumentation) :
StandardAppHelper(
instrumentation,
ActivityOptions.Pip.LABEL,
ActivityOptions.Pip.COMPONENT.toFlickerComponent()
) {
private val mediaSessionManager: MediaSessionManager
get() =
context.getSystemService(MediaSessionManager::class.java)
?: error("Could not get MediaSessionManager")
private val mediaController: MediaController?
get() =
mediaSessionManager.getActiveSessions(null).firstOrNull { it.packageName == `package` }
private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation)
open fun clickObject(resId: String) {
val selector = By.res(`package`, resId)
val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object")
obj.click()
}
/**
* Expands the PIP window my using the pinch out gesture.
*
* @param percent The percentage by which to increase the pip window size.
* @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f
*/
fun pinchOpenPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) {
// the percentage must be between 0.0f and 1.0f
if (percent <= 0.0f || percent > 1.0f) {
throw IllegalArgumentException("Percent must be between 0.0f and 1.0f")
}
val windowRect = getWindowRect(wmHelper)
// first pointer's initial x coordinate is halfway between the left edge and the center
val initLeftX = (windowRect.centerX() - windowRect.width / 4).toFloat()
// second pointer's initial x coordinate is halfway between the right edge and the center
val initRightX = (windowRect.centerX() + windowRect.width / 4).toFloat()
// horizontal distance the window should increase by
val distIncrease = windowRect.width * percent
// final x-coordinates
val finalLeftX = initLeftX - (distIncrease / 2)
val finalRightX = initRightX + (distIncrease / 2)
// y-coordinate is the same throughout this animation
val yCoord = windowRect.centerY().toFloat()
var adjustedSteps = MIN_STEPS_TO_ANIMATE
// if distance per step is at least 1, then we can use the number of steps requested
if (distIncrease.toInt() / (steps * 2) >= 1) {
adjustedSteps = steps
}
// if the distance per step is less than 1, carry out the animation in two steps
gestureHelper.pinch(
Tuple(initLeftX, yCoord), Tuple(initRightX, yCoord),
Tuple(finalLeftX, yCoord), Tuple(finalRightX, yCoord), adjustedSteps)
waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect))
}
/**
* Launches the app through an intent instead of interacting with the launcher and waits until
* the app window is in PIP mode
*/
@JvmOverloads
fun launchViaIntentAndWaitForPip(
wmHelper: WindowManagerStateHelper,
expectedWindowName: String = "",
action: String? = null,
stringExtras: Map<String, String>
) {
launchViaIntentAndWaitShown(
wmHelper,
expectedWindowName,
action,
stringExtras,
waitConditions = arrayOf(WindowManagerConditionsFactory.hasPipWindow())
)
wmHelper.StateSyncBuilder().withPipShown().waitForAndVerify()
}
/** Expand the PIP window back to full screen via intent and wait until the app is visible */
fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) =
launchViaIntentAndWaitShown(wmHelper)
fun clickEnterPipButton(wmHelper: WindowManagerStateHelper) {
clickObject(ENTER_PIP_BUTTON_ID)
// Wait on WMHelper or simply wait for 3 seconds
wmHelper.StateSyncBuilder().withPipShown().waitForAndVerify()
// when entering pip, the dismiss button is visible at the start. to ensure the pip
// animation is complete, wait until the pip dismiss button is no longer visible.
// b/176822698: dismiss-only state will be removed in the future
uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT)
}
fun enableEnterPipOnUserLeaveHint() {
clickObject(ENTER_PIP_ON_USER_LEAVE_HINT)
}
fun enableAutoEnterForPipActivity() {
clickObject(ENTER_PIP_AUTOENTER)
}
fun clickStartMediaSessionButton() {
clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID)
}
fun checkWithCustomActionsCheckbox() =
uiDevice
.findObject(By.res(`package`, WITH_CUSTOM_ACTIONS_BUTTON_ID))
?.takeIf { it.isCheckable }
?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) }
?: error("'With custom actions' checkbox not found")
fun pauseMedia() =
mediaController?.transportControls?.pause() ?: error("No active media session found")
fun stopMedia() =
mediaController?.transportControls?.stop() ?: error("No active media session found")
@Deprecated(
"Use PipAppHelper.closePipWindow(wmHelper) instead",
ReplaceWith("closePipWindow(wmHelper)")
)
open fun closePipWindow() {
closePipWindow(WindowManagerStateHelper(mInstrumentation))
}
private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect {
val windowRegion = wmHelper.getWindowRegion(this)
require(!windowRegion.isEmpty) { "Unable to find a PIP window in the current state" }
return windowRegion.bounds
}
/** Taps the pip window and dismisses it by clicking on the X button. */
open fun closePipWindow(wmHelper: WindowManagerStateHelper) {
val windowRect = getWindowRect(wmHelper)
uiDevice.click(windowRect.centerX(), windowRect.centerY())
// search and interact with the dismiss button
val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss")
uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT)
val dismissPipObject =
uiDevice.findObject(dismissSelector) ?: error("PIP window dismiss button not found")
val dismissButtonBounds = dismissPipObject.visibleBounds
uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY())
// Wait for animation to complete.
wmHelper.StateSyncBuilder().withPipGone().withHomeActivityVisible().waitForAndVerify()
}
/** Close the pip window by pressing the expand button */
fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) {
val windowRect = getWindowRect(wmHelper)
uiDevice.click(windowRect.centerX(), windowRect.centerY())
// search and interact with the expand button
val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button")
uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT)
val expandPipObject =
uiDevice.findObject(expandSelector) ?: error("PIP window expand button not found")
val expandButtonBounds = expandPipObject.visibleBounds
uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY())
wmHelper.StateSyncBuilder().withPipGone().withFullScreenApp(this).waitForAndVerify()
}
/** Double click on the PIP window to expand it */
fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) {
val windowRect = getWindowRect(wmHelper)
Log.d(TAG, "First click")
uiDevice.click(windowRect.centerX(), windowRect.centerY())
Log.d(TAG, "Second click")
uiDevice.click(windowRect.centerX(), windowRect.centerY())
Log.d(TAG, "Wait for app transition to end")
wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect))
}
private fun waitForPipWindowToExpandFrom(
wmHelper: WindowManagerStateHelper,
windowRect: Region
) {
wmHelper
.StateSyncBuilder()
.add("pipWindowExpanded") {
val pipAppWindow =
it.wmState.visibleWindows.firstOrNull { window ->
this.windowMatchesAnyOf(window)
}
?: return@add false
val pipRegion = pipAppWindow.frameRegion
return@add pipRegion.coversMoreThan(windowRect)
}
.waitForAndVerify()
}
companion object {
private const val TAG = "PipAppHelper"
private const val ENTER_PIP_BUTTON_ID = "enter_pip"
private const val WITH_CUSTOM_ACTIONS_BUTTON_ID = "with_custom_actions"
private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start"
private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual"
private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter"
// minimum number of steps to take, when animating gestures, needs to be 2
// so that there is at least a single intermediate layer that flicker tests can check
private const val MIN_STEPS_TO_ANIMATE = 2
}
}