blob: 07721277902552550621bffbb9170d42cc5e93ec [file] [log] [blame]
/*
* Copyright 2021 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.
*/
// Sidecar is deprecated but consuming code must be maintained for compatibility reasons
@file:Suppress("DEPRECATION")
package androidx.window.layout.adapter.sidecar
import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.os.IBinder
import android.view.View
import android.view.Window
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.window.TestWindow
import androidx.window.WindowTestBase
import androidx.window.core.Bounds
import androidx.window.layout.FoldingFeature
import androidx.window.layout.adapter.sidecar.ExtensionInterfaceCompat.ExtensionCallbackInterface
import androidx.window.layout.FoldingFeature.State.Companion.FLAT
import androidx.window.layout.FoldingFeature.State.Companion.HALF_OPENED
import androidx.window.layout.HardwareFoldingFeature
import androidx.window.layout.HardwareFoldingFeature.Type.Companion.FOLD
import androidx.window.layout.TestFoldingFeatureUtil.invalidFoldBounds
import androidx.window.layout.TestFoldingFeatureUtil.validFoldBound
import androidx.window.layout.WindowLayoutInfo
import androidx.window.sidecar.SidecarDeviceState
import androidx.window.sidecar.SidecarDisplayFeature
import androidx.window.sidecar.SidecarInterface
import androidx.window.sidecar.SidecarInterface.SidecarCallback
import androidx.window.sidecar.SidecarWindowLayoutInfo
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argThat
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.atLeastOnce
import com.nhaarman.mockitokotlin2.doAnswer
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.reset
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* Tests for [SidecarCompat] implementation of [ExtensionInterfaceCompat]. This class
* uses a mocked Sidecar to verify the behavior of the implementation on any hardware.
*
*/
@LargeTest
@RunWith(AndroidJUnit4::class)
class SidecarCompatTest : WindowTestBase() {
private lateinit var activity: Activity
private lateinit var sidecarCompat: SidecarCompat
@Before
fun setUp() {
activity = mock()
sidecarCompat = SidecarCompat(mock(), SidecarAdapter())
whenever(activity.getResources())
.thenReturn(ApplicationProvider.getApplicationContext<Context>().resources)
val window: Window = spy(TestWindow(activity))
window.attributes.token = mock()
whenever(activity.getWindow()).thenReturn(window)
// Setup mocked sidecar responses
val defaultDeviceState = SidecarDeviceState()
SidecarAdapter.setSidecarDevicePosture(
defaultDeviceState,
SidecarDeviceState.POSTURE_HALF_OPENED
)
whenever(sidecarCompat.sidecar!!.deviceState).thenReturn(defaultDeviceState)
val sidecarDisplayFeature = newDisplayFeature(
Rect(0, 1, WINDOW_BOUNDS.width(), 1), SidecarDisplayFeature.TYPE_HINGE
)
val sidecarWindowLayoutInfo = SidecarWindowLayoutInfo()
val displayFeatures = listOf(sidecarDisplayFeature)
SidecarAdapter.setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures)
whenever(sidecarCompat.sidecar!!.getWindowLayoutInfo(any()))
.thenReturn(sidecarWindowLayoutInfo)
}
@Test
fun testGetWindowLayout() {
val fakeSidecarImp = FakeExtensionImp(
newDeviceState(SidecarDeviceState.POSTURE_OPENED),
newWindowLayoutInfo(emptyList())
)
val compat = SidecarCompat(fakeSidecarImp, SidecarAdapter())
val mockCallback = mock<ExtensionCallbackInterface>()
compat.setExtensionCallback(mockCallback)
compat.onWindowLayoutChangeListenerAdded(activity)
fakeSidecarImp.triggerGoodSignal()
verify(mockCallback, atLeastOnce()).onWindowLayoutChanged(eq(activity), any())
}
@Test
public fun testGetWindowLayout_featureWithEmptyBounds() {
// Add a feature with an empty bounds to the reported list
val originalWindowLayoutInfo = sidecarCompat.sidecar!!.getWindowLayoutInfo(
getActivityWindowToken(
activity
)
)
val sidecarDisplayFeatures =
SidecarAdapter.getSidecarDisplayFeatures(originalWindowLayoutInfo)
val newFeature = SidecarDisplayFeature()
newFeature.rect = Rect()
SidecarAdapter.setSidecarDisplayFeatures(
originalWindowLayoutInfo,
sidecarDisplayFeatures + newFeature
)
// Verify that this feature is skipped.
val windowLayoutInfo = sidecarCompat.getWindowLayoutInfo(activity)
assertEquals(
(sidecarDisplayFeatures.size).toLong(),
windowLayoutInfo.displayFeatures.size.toLong()
)
}
@Test
public fun testGetWindowLayout_foldWithNonZeroArea() {
val originalWindowLayoutInfo = sidecarCompat.sidecar!!.getWindowLayoutInfo(mock())
val sidecarDisplayFeatures =
SidecarAdapter.getSidecarDisplayFeatures(originalWindowLayoutInfo)
val additionalFeatures = listOf(
// Horizontal fold.
newDisplayFeature(
Rect(0, 1, WINDOW_BOUNDS.width(), 2),
SidecarDisplayFeature.TYPE_FOLD
),
// Vertical fold.
newDisplayFeature(
Rect(1, 0, 2, WINDOW_BOUNDS.height()),
SidecarDisplayFeature.TYPE_FOLD
)
)
val replacementFeatures = sidecarDisplayFeatures + additionalFeatures
SidecarAdapter.setSidecarDisplayFeatures(originalWindowLayoutInfo, replacementFeatures)
// Verify that these features are skipped.
val windowLayoutInfo = sidecarCompat.getWindowLayoutInfo(activity)
assertEquals(
(sidecarDisplayFeatures.size).toLong(),
windowLayoutInfo.displayFeatures.size.toLong()
)
}
@Test
public fun testGetWindowLayout_hingeNotSpanningEntireWindow() {
val originalWindowLayoutInfo = sidecarCompat.sidecar!!.getWindowLayoutInfo(mock())
val sidecarDisplayFeatures =
SidecarAdapter.getSidecarDisplayFeatures(originalWindowLayoutInfo)
val additionalFeatures = listOf(
// Horizontal hinge.
newDisplayFeature(
Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
SidecarDisplayFeature.TYPE_FOLD
),
// Vertical hinge.
newDisplayFeature(
Rect(1, 0, 2, WINDOW_BOUNDS.height() - 1),
SidecarDisplayFeature.TYPE_FOLD
)
)
val replacementFeatures = sidecarDisplayFeatures + additionalFeatures
SidecarAdapter.setSidecarDisplayFeatures(originalWindowLayoutInfo, replacementFeatures)
// Verify that these features are skipped.
val windowLayoutInfo = sidecarCompat.getWindowLayoutInfo(activity)
assertEquals(
(sidecarDisplayFeatures.size).toLong(),
windowLayoutInfo.displayFeatures.size.toLong()
)
}
@Test
public fun testGetWindowLayout_foldNotSpanningEntireWindow() {
val originalWindowLayoutInfo = sidecarCompat.sidecar!!.getWindowLayoutInfo(mock())
val sidecarDisplayFeatures =
SidecarAdapter.getSidecarDisplayFeatures(originalWindowLayoutInfo)
val additionalFeatures = listOf(
// Horizontal fold.
newDisplayFeature(
Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
SidecarDisplayFeature.TYPE_FOLD
),
// Vertical fold.
newDisplayFeature(
Rect(1, 0, 2, WINDOW_BOUNDS.height() - 1),
SidecarDisplayFeature.TYPE_FOLD
)
)
val replacementFeatures = sidecarDisplayFeatures + additionalFeatures
SidecarAdapter.setSidecarDisplayFeatures(originalWindowLayoutInfo, replacementFeatures)
// Verify that these features are skipped.
val windowLayoutInfo = sidecarCompat.getWindowLayoutInfo(activity)
assertEquals(
(sidecarDisplayFeatures.size).toLong(),
windowLayoutInfo.displayFeatures.size.toLong()
)
}
@Test
fun testExtensionCallback_filterRemovesInvalidValues() {
val fakeSidecarImp = FakeExtensionImp(
newDeviceState(SidecarDeviceState.POSTURE_OPENED),
newWindowLayoutInfo(emptyList())
)
val compat = SidecarCompat(fakeSidecarImp, SidecarAdapter())
val mockCallback = mock<ExtensionCallbackInterface>()
compat.setExtensionCallback(mockCallback)
compat.onWindowLayoutChangeListenerAdded(activity)
fakeSidecarImp.triggerMalformedSignal()
verify(mockCallback).onWindowLayoutChanged(
any(),
argThat { windowLayoutInfo -> windowLayoutInfo.displayFeatures.isEmpty() }
)
}
@Test
fun testSetExtensionCallback() {
val sidecarCallbackCaptor = argumentCaptor<SidecarCallback>()
// Verify that the sidecar got the callback set
val callback = mock<ExtensionCallbackInterface>()
sidecarCompat.setExtensionCallback(callback)
verify(sidecarCompat.sidecar!!)
.setSidecarCallback(sidecarCallbackCaptor.capture())
// Verify that the callback set for sidecar propagates the device state callback
val sidecarDeviceState = SidecarDeviceState()
SidecarAdapter.setSidecarDevicePosture(
sidecarDeviceState,
SidecarDeviceState.POSTURE_HALF_OPENED
)
sidecarCallbackCaptor.firstValue.onDeviceStateChanged(sidecarDeviceState)
// Verify that the callback set for sidecar propagates the window layout callback when a
// window layout changed listener has been added.
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
val bounds = Rect(0, 1, WINDOW_BOUNDS.width(), 1)
val sidecarDisplayFeature = newDisplayFeature(
bounds,
SidecarDisplayFeature.TYPE_HINGE
)
val sidecarWindowLayoutInfo = SidecarWindowLayoutInfo()
val displayFeatures = listOf(sidecarDisplayFeature)
SidecarAdapter.setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures)
sidecarCallbackCaptor.firstValue.onWindowLayoutChanged(
getActivityWindowToken(
activity
),
sidecarWindowLayoutInfo
)
val windowLayoutInfoCaptor = argumentCaptor<WindowLayoutInfo>()
verify(callback).onWindowLayoutChanged(eq(activity), windowLayoutInfoCaptor.capture())
val capturedLayout = windowLayoutInfoCaptor.firstValue
assertEquals(1, capturedLayout.displayFeatures.size.toLong())
val capturedDisplayFeature = capturedLayout.displayFeatures[0]
val foldingFeature = capturedDisplayFeature as FoldingFeature
assertNotNull(foldingFeature)
assertEquals(bounds, capturedDisplayFeature.bounds)
}
@Test
public fun testMissingCallToOnWindowLayoutChangedListenerAdded() {
val sidecarCallbackCaptor = argumentCaptor<SidecarCallback>()
// Verify that the sidecar got the callback set
val callback = mock<ExtensionCallbackInterface>()
sidecarCompat.setExtensionCallback(callback)
verify(sidecarCompat.sidecar!!)
.setSidecarCallback(sidecarCallbackCaptor.capture())
// Verify that the callback set for sidecar propagates the window layout callback when a
// window layout changed listener has been added.
val sidecarDisplayFeature = SidecarDisplayFeature()
sidecarDisplayFeature.type = SidecarDisplayFeature.TYPE_HINGE
val bounds = Rect(1, 2, 3, 4)
sidecarDisplayFeature.rect = bounds
val sidecarWindowLayoutInfo = SidecarWindowLayoutInfo()
val displayFeatures = listOf(sidecarDisplayFeature)
SidecarAdapter.setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures)
val windowToken = mock<IBinder>()
sidecarCallbackCaptor.firstValue.onWindowLayoutChanged(
windowToken,
sidecarWindowLayoutInfo
)
verifyZeroInteractions(callback)
}
@Test
fun testOnWindowLayoutChangeListenerAdded() {
val windowToken = getActivityWindowToken(
activity
)
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
verify(sidecarCompat.sidecar!!).onWindowLayoutChangeListenerAdded(windowToken)
verify(sidecarCompat.sidecar!!).onDeviceStateListenersChanged(false)
}
@Test
public fun testOnWindowLayoutChangeListenerAdded_emitInitialValue() {
val layoutInfo = SidecarWindowLayoutInfo()
val expectedLayoutInfo = WindowLayoutInfo(listOf())
val listener = mock<ExtensionCallbackInterface>()
sidecarCompat.setExtensionCallback(listener)
whenever(sidecarCompat.sidecar!!.getWindowLayoutInfo(any()))
.thenReturn(layoutInfo)
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
verify(listener).onWindowLayoutChanged(
activity, expectedLayoutInfo
)
}
@Test
fun testOnWindowLayoutChangeListenerAdded_emitInitialValueDelayed() {
val layoutInfo = SidecarWindowLayoutInfo()
val expectedLayoutInfo = WindowLayoutInfo(listOf())
val listener = mock<ExtensionCallbackInterface>()
sidecarCompat.setExtensionCallback(listener)
whenever(sidecarCompat.sidecar!!.getWindowLayoutInfo(any()))
.thenReturn(layoutInfo)
val fakeView = mock<View>()
val fakeWindow: Window = TestWindow(activity, fakeView)
doAnswer { invocation ->
val stateChangeListener = invocation.getArgument<View.OnAttachStateChangeListener>(0)
fakeWindow.attributes.token = mock()
stateChangeListener.onViewAttachedToWindow(fakeView)
null
}.whenever(fakeView).addOnAttachStateChangeListener(any())
whenever(activity.window).thenReturn(fakeWindow)
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
verify(listener).onWindowLayoutChanged(
activity, expectedLayoutInfo
)
}
@Test
fun testOnWindowLayoutInfoChanged_emitWhenResubscribe() {
val layoutInfo = SidecarWindowLayoutInfo()
val expectedLayoutInfo = WindowLayoutInfo(listOf())
val listener = mock<ExtensionCallbackInterface>()
sidecarCompat.setExtensionCallback(listener)
whenever(sidecarCompat.sidecar!!.getWindowLayoutInfo(any()))
.thenReturn(layoutInfo)
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
sidecarCompat.onWindowLayoutChangeListenerRemoved(activity)
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
verify(listener, times(2)).onWindowLayoutChanged(
activity, expectedLayoutInfo
)
}
@Test
fun testOnWindowLayoutInfoChanged_emitNewValueWhenResubscribe() {
val layoutInfo = SidecarWindowLayoutInfo()
val expectedLayoutInfo = WindowLayoutInfo(listOf())
val expectedLayoutInfo2 = WindowLayoutInfo(listOf(
HardwareFoldingFeature(
Bounds(validFoldBound(WINDOW_BOUNDS)),
FOLD,
HALF_OPENED
)
))
val listener = mock<ExtensionCallbackInterface>()
sidecarCompat.setExtensionCallback(listener)
whenever(sidecarCompat.sidecar!!.getWindowLayoutInfo(any()))
.thenReturn(layoutInfo)
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
verify(listener).onWindowLayoutChanged(activity, expectedLayoutInfo)
// remove listener
sidecarCompat.onWindowLayoutChangeListenerRemoved(activity)
// change the value for new subscriber
whenever(sidecarCompat.sidecar!!.getWindowLayoutInfo(any()))
.thenReturn(
newWindowLayoutInfo(listOf(
newDisplayFeature(
validFoldBound(WINDOW_BOUNDS),
SidecarDisplayFeature.TYPE_FOLD
)
))
)
// resubscribe
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
verify(listener).onWindowLayoutChanged(activity, expectedLayoutInfo2)
}
@Test
fun testOnWindowLayoutChangeListenerRemoved() {
val windowToken = getActivityWindowToken(
activity
)
sidecarCompat.onWindowLayoutChangeListenerRemoved(activity)
verify(sidecarCompat.sidecar!!).onWindowLayoutChangeListenerRemoved(
eq(windowToken)
)
}
@Test
fun testOnDeviceStateListenersChanged() {
sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
sidecarCompat.onWindowLayoutChangeListenerRemoved(activity)
verify(sidecarCompat.sidecar!!).onDeviceStateListenersChanged(true)
}
@Test
fun testOnDeviceStateChangedUpdatesWindowLayout() {
val fakeSidecarImp = FakeExtensionImp(
newDeviceState(SidecarDeviceState.POSTURE_CLOSED),
validWindowLayoutInfo()
)
val compat = SidecarCompat(fakeSidecarImp, SidecarAdapter())
val mockCallback = mock<ExtensionCallbackInterface>()
compat.setExtensionCallback(mockCallback)
compat.onWindowLayoutChangeListenerAdded(activity)
val windowLayoutCaptor = argumentCaptor<WindowLayoutInfo>()
reset(mockCallback)
fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_OPENED))
verify(mockCallback).onWindowLayoutChanged(eq(activity), windowLayoutCaptor.capture())
var capturedFoldingFeature = windowLayoutCaptor.firstValue
.displayFeatures[0] as FoldingFeature
assertEquals(FLAT, capturedFoldingFeature.state)
reset(mockCallback)
fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_HALF_OPENED))
verify(mockCallback)
.onWindowLayoutChanged(eq(activity), windowLayoutCaptor.capture())
capturedFoldingFeature = windowLayoutCaptor.secondValue.displayFeatures[0] as FoldingFeature
assertEquals(HALF_OPENED, capturedFoldingFeature.state)
// No display features must be reported in closed state or flipped state.
reset(mockCallback)
fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_CLOSED))
fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_FLIPPED))
verify(mockCallback)
.onWindowLayoutChanged(eq(activity), windowLayoutCaptor.capture())
assertTrue(windowLayoutCaptor.thirdValue.displayFeatures.isEmpty())
}
private class FakeExtensionImp(
private var deviceState: SidecarDeviceState,
private var info: SidecarWindowLayoutInfo
) : SidecarInterface {
private var callback: SidecarCallback
private val tokens = mutableListOf<IBinder>()
override fun setSidecarCallback(callback: SidecarCallback) {
this.callback = callback
}
override fun getWindowLayoutInfo(windowToken: IBinder): SidecarWindowLayoutInfo {
return info
}
override fun onWindowLayoutChangeListenerAdded(windowToken: IBinder) {
tokens.add(windowToken)
}
override fun onWindowLayoutChangeListenerRemoved(windowToken: IBinder) {
tokens.remove(windowToken)
}
override fun getDeviceState(): SidecarDeviceState {
return deviceState
}
override fun onDeviceStateListenersChanged(isEmpty: Boolean) {}
fun triggerMalformedSignal() {
triggerSignal(malformedWindowLayoutInfo())
}
fun triggerGoodSignal() {
triggerSignal(validWindowLayoutInfo())
}
fun triggerSignal(info: SidecarWindowLayoutInfo) {
this.info = info
for (token in tokens) {
callback.onWindowLayoutChanged(token, info)
}
}
fun triggerDeviceState(state: SidecarDeviceState) {
deviceState = state
callback.onDeviceStateChanged(state)
}
private fun malformedWindowLayoutInfo(): SidecarWindowLayoutInfo {
val malformedFeatures = mutableListOf<SidecarDisplayFeature>()
for (malformedBound in invalidFoldBounds(WINDOW_BOUNDS)) {
malformedFeatures.add(
newDisplayFeature(
malformedBound,
SidecarDisplayFeature.TYPE_FOLD
)
)
}
for (malformedBound in invalidFoldBounds(WINDOW_BOUNDS)) {
malformedFeatures.add(
newDisplayFeature(
malformedBound,
SidecarDisplayFeature.TYPE_HINGE
)
)
}
return newWindowLayoutInfo(malformedFeatures)
}
init {
callback = object : SidecarCallback {
override fun onDeviceStateChanged(newDeviceState: SidecarDeviceState) {}
override fun onWindowLayoutChanged(
windowToken: IBinder,
newLayout: SidecarWindowLayoutInfo
) {
}
}
}
}
private companion object {
private val WINDOW_BOUNDS = Rect(1, 1, 50, 100)
private fun newDisplayFeature(rect: Rect, type: Int): SidecarDisplayFeature {
val feature = SidecarDisplayFeature()
feature.rect = rect
feature.type = type
return feature
}
private fun newWindowLayoutInfo(
features: List<SidecarDisplayFeature>
): SidecarWindowLayoutInfo {
val info = SidecarWindowLayoutInfo()
SidecarAdapter.setSidecarDisplayFeatures(info, features)
return info
}
private fun validWindowLayoutInfo(): SidecarWindowLayoutInfo {
val goodFeatures = listOf(
newDisplayFeature(
validFoldBound(WINDOW_BOUNDS),
SidecarDisplayFeature.TYPE_FOLD
)
)
return newWindowLayoutInfo(goodFeatures)
}
private fun newDeviceState(posture: Int): SidecarDeviceState {
val state = SidecarDeviceState()
SidecarAdapter.setSidecarDevicePosture(state, posture)
return state
}
}
}