blob: 5fc05fce1a5c0b28df7104a7da776d2b2b774b51 [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 android.systemui.tv.cts
import android.Manifest.permission.READ_DREAM_STATE
import android.Manifest.permission.WRITE_DREAM_STATE
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
import android.content.ComponentName
import android.graphics.Point
import android.graphics.Rect
import android.os.ServiceManager
import android.platform.test.annotations.Postsubmit
import android.server.wm.Condition
import android.server.wm.annotation.Group2
import android.service.dreams.DreamService
import android.service.dreams.IDreamManager
import android.systemui.tv.cts.Components.PIP_ACTIVITY
import android.systemui.tv.cts.Components.PIP_MENU_ACTIVITY
import android.systemui.tv.cts.Components.windowName
import android.systemui.tv.cts.PipActivity.ACTION_ENTER_PIP
import android.systemui.tv.cts.PipActivity.EXTRA_ASPECT_RATIO_DENOMINATOR
import android.systemui.tv.cts.PipActivity.EXTRA_ASPECT_RATIO_NUMERATOR
import android.systemui.tv.cts.PipActivity.Ratios.MAX_ASPECT_RATIO_DENOMINATOR
import android.systemui.tv.cts.PipActivity.Ratios.MAX_ASPECT_RATIO_NUMERATOR
import android.systemui.tv.cts.PipActivity.Ratios.MIN_ASPECT_RATIO_DENOMINATOR
import android.systemui.tv.cts.PipActivity.Ratios.MIN_ASPECT_RATIO_NUMERATOR
import android.systemui.tv.cts.ResourceNames.ID_PIP_MENU_CLOSE_BUTTON
import android.systemui.tv.cts.ResourceNames.ID_PIP_MENU_FULLSCREEN_BUTTON
import android.systemui.tv.cts.ResourceNames.ID_PIP_MENU_PLAY_PAUSE_BUTTON
import android.util.Size
import android.view.Gravity
import android.view.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import com.android.compatibility.common.util.SystemUtil
import com.android.compatibility.common.util.ThrowingSupplier
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Tests most basic picture in picture (PiP) behavior.
*
* Build/Install/Run:
* atest CtsSystemUiTestCases:BasicPipTests
*/
@Postsubmit
@Group2
@RunWith(AndroidJUnit4::class)
class BasicPipTests : PipTestBase() {
private val pipGravity: Int = resources.getInteger(
com.android.internal.R.integer.config_defaultPictureInPictureGravity)
private val displaySize = windowManager.maximumWindowMetrics.bounds
private val defaultPipAspectRatio: Float = resources.getFloat(
com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio)
private val minPipAspectRatio: Float = resources.getFloat(
com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio)
private val maxPipAspectRatio: Float = resources.getFloat(
com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio)
private val defaultPipHeight: Float = resources.getDimension(
com.android.internal.R.dimen.default_minimal_size_pip_resizable_task)
private val screenEdgeInsetString = resources.getString(
com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets)
private val screenEdgeInsets: Point = Size.parseSize(screenEdgeInsetString).let {
val displayMetrics = resources.displayMetrics
Point(dipToPx(it.width, displayMetrics), dipToPx(it.height, displayMetrics))
}
@After
fun tearDown() {
stopPackage(PIP_ACTIVITY)
}
/** Open an app in pip mode and ensure it has a window but is not focused. */
@Test
fun openPip_launchedNotFocused() {
launchActivity(PIP_ACTIVITY, ACTION_ENTER_PIP)
waitForEnterPip(PIP_ACTIVITY)
assertLaunchedNotFocused(PIP_ACTIVITY)
}
/** Ensure an app can be launched into pip mode from the screensaver state. */
@Test
fun openPip_afterScreenSaver() {
runWithDreamManager { dreamManager ->
dreamManager.dream()
dreamManager.waitForDream()
}
// Launch pip activity that is supposed to wake up the device
launchActivity(
activity = PIP_ACTIVITY,
action = ACTION_ENTER_PIP,
boolExtras = mapOf(PipActivity.EXTRA_TURN_ON_SCREEN to true)
)
waitForEnterPip(PIP_ACTIVITY)
assertLaunchedNotFocused(PIP_ACTIVITY)
assertTrue("Device must be awake") {
runWithDreamManager { dreamManager ->
!dreamManager.isDreaming
}
}
}
/** Ensure an app in pip mode remains open throughout the device dreaming and waking. */
@Test
fun pipApp_remainsOpen_afterScreensaver() {
launchActivity(PIP_ACTIVITY, ACTION_ENTER_PIP)
waitForEnterPip(PIP_ACTIVITY)
runWithDreamManager { dreamManager ->
dreamManager.dream()
dreamManager.waitForDream()
dreamManager.awaken()
dreamManager.waitForAwake()
}
assertLaunchedNotFocused(PIP_ACTIVITY)
}
/** Open an app in pip mode and ensure it is located at the expected default position. */
@Test
fun openPip_position_defaultAspectRatio() {
launchActivity(PIP_ACTIVITY, ACTION_ENTER_PIP)
assertPipWindowPosition(PIP_ACTIVITY, defaultPipAspectRatio)
}
/** Open an app in pip mode with minimal aspect ratio and ensure its position is correct. */
@Test
fun openPip_position_minAspectRatio() {
launchActivity(
PIP_ACTIVITY,
ACTION_ENTER_PIP,
intExtras = mapOf(
EXTRA_ASPECT_RATIO_NUMERATOR to MIN_ASPECT_RATIO_NUMERATOR,
EXTRA_ASPECT_RATIO_DENOMINATOR to MIN_ASPECT_RATIO_DENOMINATOR
)
)
assertPipWindowPosition(PIP_ACTIVITY, minPipAspectRatio)
}
/** Open an app in pip mode with maximal aspect ratio and ensure its position is correct. */
@Test
fun openPip_position_maxAspectRatio() {
launchActivity(
PIP_ACTIVITY,
ACTION_ENTER_PIP,
intExtras = mapOf(
EXTRA_ASPECT_RATIO_NUMERATOR to MAX_ASPECT_RATIO_NUMERATOR,
EXTRA_ASPECT_RATIO_DENOMINATOR to MAX_ASPECT_RATIO_DENOMINATOR
)
)
assertPipWindowPosition(PIP_ACTIVITY, maxPipAspectRatio)
}
/** Open an app in pip mode and ensure its pip menu can be opened. */
@Test
fun pipMenu_open() {
launchPipThenEnterMenu()
assertPipMenuOpen()
}
/** Ensure the [android.view.KeyEvent.KEYCODE_WINDOW] correctly opens the pip menu. */
@Test
fun pipMenu_open_onWindowButtonPress() {
launchActivity(PIP_ACTIVITY, ACTION_ENTER_PIP)
waitForEnterPip(PIP_ACTIVITY)
// enter pip menu
uiDevice.pressKeyCode(KeyEvent.KEYCODE_WINDOW)
assertPipMenuOpen()
}
/** Ensure the pip menu opens in the expected location. */
@Test
fun pipMenu_correctLocation() {
launchPipThenEnterMenu()
wmState.waitFor("The PiP menu must be in the right place!") {
val pipTask = it.getTaskByActivity(PIP_ACTIVITY, WINDOWING_MODE_PINNED)
pipTask.bounds == menuModePipBounds
} || error("The PiP activity is not in the right place when the menu is shown!")
}
/** Open an app's pip menu then press its close button and ensure the app is closed. */
@Test
fun pipMenu_openThenClose() {
launchPipThenEnterMenu()
val closeButton = locateByResourceName(ID_PIP_MENU_CLOSE_BUTTON)
closeButton.click()
wmState.waitFor("The PiP app and its menu must be closed!") {
it.containsNoneOf(listOf(PIP_ACTIVITY, PIP_MENU_ACTIVITY))
}
}
/** Open an app's pip menu then press its fullscreen button and ensure the app is fullscreen. */
@Test
fun pipMenu_openThenFullscreen() {
launchPipThenEnterMenu()
val fullscreenButton = locateByResourceName(ID_PIP_MENU_FULLSCREEN_BUTTON)
fullscreenButton.click()
waitForFullscreen(PIP_ACTIVITY)
wmState.waitAndAssertActivityRemoved(PIP_MENU_ACTIVITY)
wmState.assertFocusedActivity("The PiP app must be focused!", PIP_ACTIVITY)
assertTrue("The PiP app must be in fullscreen mode!") {
wmState.containsActivityInWindowingMode(PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN)
}
}
/** Ensure the pip menu contains a media control button when there is playback. */
@Test
fun pipMenu_containsMediaButton() {
// launch a pip app, activate its media session, and start media playback
launchActivity(
activity = PIP_ACTIVITY,
action = PipActivity.ACTION_MEDIA_PLAY,
boolExtras = mapOf(
PipActivity.EXTRA_ENTER_PIP to true,
PipActivity.EXTRA_MEDIA_SESSION_ACTIVE to true
),
stringExtras = mapOf(PipActivity.EXTRA_MEDIA_SESSION_TITLE to "Playback")
)
waitForEnterPip(PIP_ACTIVITY)
// enter pip menu
sendBroadcast(PipMenu.ACTION_MENU)
waitForFullscreen(PIP_MENU_ACTIVITY)
assertPipMenuOpen()
// the media control button has to be present in the pip menu
locateByResourceName(ID_PIP_MENU_PLAY_PAUSE_BUTTON)
}
/** Open an app's pip menu then press back and ensure the app is back in pip. */
@Test
fun pipMenu_openThenBack() {
launchPipThenEnterMenu()
uiDevice.pressBack()
assertActivityInPip(PIP_ACTIVITY)
}
/** Open an app's pip menu then press home and ensure the app is back in pip. */
@Test
fun pipMenu_openThenHome() {
launchPipThenEnterMenu()
uiDevice.pressHome()
assertActivityInPip(PIP_ACTIVITY)
}
/** Assert that the given activity is in pip mode and the pip menu is gone. */
private fun assertActivityInPip(activity: ComponentName) {
wmState.waitAndAssertActivityRemoved(PIP_MENU_ACTIVITY)
wmState.assertNotFocusedActivity("The PiP app must not be focused!", activity)
assertTrue("The PiP app must be back in pip mode after dismissing the pip menu!") {
wmState.containsActivityInWindowingMode(activity, WINDOWING_MODE_PINNED)
}
}
/** Launches an app into pip mode then opens the pip menu. */
private fun launchPipThenEnterMenu() {
launchActivity(PIP_ACTIVITY, ACTION_ENTER_PIP)
waitForEnterPip(PIP_ACTIVITY)
// enter pip menu
sendBroadcast(PipMenu.ACTION_MENU)
waitForFullscreen(PIP_MENU_ACTIVITY)
}
/** Ensure the pip window has the correct dimensions and position for a given [aspectRatio]. */
private fun assertPipWindowPosition(activity: ComponentName, aspectRatio: Float) {
waitForEnterPip(PIP_ACTIVITY)
val pipTask = wmState.getTaskByActivity(activity, WINDOWING_MODE_PINNED)
assertEquals(
expected = expectedPipBounds(aspectRatio),
actual = pipTask.bounds,
message = "The PiP window must be at the expected location!"
)
}
/** Calculates the pip window bounds given the [aspectRatio]. */
private fun expectedPipBounds(aspectRatio: Float): Rect = Rect().apply {
Gravity.apply(pipGravity, (defaultPipHeight * aspectRatio).toInt(),
defaultPipHeight.toInt(),
displaySize, screenEdgeInsets.x, screenEdgeInsets.y, this)
}
private fun assertLaunchedNotFocused(activity: ComponentName) {
wmState.assertActivityDisplayed(activity)
wmState.assertNotFocusedWindow(
"PiP Window must not be focused!",
activity.windowName()
)
}
/** Run the given actions on a dream manager, acquiring appropriate permissions. */
private fun <T> runWithDreamManager(actions: (IDreamManager) -> T): T {
val dreamManager: IDreamManager = IDreamManager.Stub.asInterface(
ServiceManager.getServiceOrThrow(DreamService.DREAM_SERVICE))
return SystemUtil.runWithShellPermissionIdentity(ThrowingSupplier {
actions(dreamManager)
}, READ_DREAM_STATE, WRITE_DREAM_STATE)
}
/** Wait for the device to enter dream state. Throw on timeout. */
private fun IDreamManager.waitForDream() {
val message = "Device must be dreaming!"
Condition.waitFor(message) {
isDreaming
} || error(message)
}
/** Wait for the device to awaken. Throw on timeout. */
private fun IDreamManager.waitForAwake() {
val message = "Device must be awake!"
Condition.waitFor(message) {
!isDreaming
} || error(message)
}
private fun locateByResourceName(resourceName: String): UiObject2 =
uiDevice.wait(Until.findObject(By.res(resourceName)), defaultTimeout)
?: error("Could not locate $resourceName")
}