blob: 87fca1f23f1a7c57f143ae07e4cd62f7b4d20b53 [file] [log] [blame]
/*
* Copyright (C) 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.
*/
package com.android.systemui.unfold.updates
import android.os.Handler
import android.testing.AndroidTestingRunner
import androidx.core.util.Consumer
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider
import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import java.util.concurrent.Executor
import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidTestingRunner::class)
@SmallTest
class DeviceFoldStateProviderTest : SysuiTestCase() {
@Mock
private lateinit var activityTypeProvider: ActivityManagerActivityTypeProvider
@Mock
private lateinit var handler: Handler
private val foldProvider = TestFoldProvider()
private val screenOnStatusProvider = TestScreenOnStatusProvider()
private val testHingeAngleProvider = TestHingeAngleProvider()
private lateinit var foldStateProvider: DeviceFoldStateProvider
private val foldUpdates: MutableList<Int> = arrayListOf()
private val hingeAngleUpdates: MutableList<Float> = arrayListOf()
private var scheduledRunnable: Runnable? = null
private var scheduledRunnableDelay: Long? = null
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val config = object : UnfoldTransitionConfig by ResourceUnfoldTransitionConfig() {
override val halfFoldedTimeoutMillis: Int
get() = HALF_OPENED_TIMEOUT_MILLIS.toInt()
}
foldStateProvider =
DeviceFoldStateProvider(
config,
testHingeAngleProvider,
screenOnStatusProvider,
foldProvider,
activityTypeProvider,
context.mainExecutor,
handler
)
foldStateProvider.addCallback(
object : FoldStateProvider.FoldUpdatesListener {
override fun onHingeAngleUpdate(angle: Float) {
hingeAngleUpdates.add(angle)
}
override fun onFoldUpdate(update: Int) {
foldUpdates.add(update)
}
})
foldStateProvider.start()
whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock ->
scheduledRunnable = invocationOnMock.getArgument<Runnable>(0)
scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1)
null
}
whenever(handler.removeCallbacks(any<Runnable>())).then { invocationOnMock ->
val removedRunnable = invocationOnMock.getArgument<Runnable>(0)
if (removedRunnable == scheduledRunnable) {
scheduledRunnableDelay = null
scheduledRunnable = null
}
null
}
// By default, we're on launcher.
setupForegroundActivityType(isHomeActivity = true)
}
@Test
fun testOnFolded_emitsFinishClosedEvent() {
setFoldState(folded = true)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
}
@Test
fun testOnUnfolded_emitsStartOpeningEvent() {
setFoldState(folded = false)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING)
}
@Test
fun testOnFolded_stopsHingeAngleProvider() {
setFoldState(folded = true)
assertThat(testHingeAngleProvider.isStarted).isFalse()
}
@Test
fun testOnUnfolded_startsHingeAngleProvider() {
setFoldState(folded = false)
assertThat(testHingeAngleProvider.isStarted).isTrue()
}
@Test
fun testFirstScreenOnEventWhenFolded_doesNotEmitEvents() {
setFoldState(folded = true)
foldUpdates.clear()
fireScreenOnEvent()
// Power button turn on
assertThat(foldUpdates).isEmpty()
}
@Test
fun testFirstScreenOnEventWhenUnfolded_doesNotEmitEvents() {
setFoldState(folded = false)
foldUpdates.clear()
fireScreenOnEvent()
assertThat(foldUpdates).isEmpty()
}
@Test
fun testFirstScreenOnEventAfterFoldAndUnfold_emitsUnfoldedScreenAvailableEvent() {
setFoldState(folded = false)
setFoldState(folded = true)
fireScreenOnEvent()
setFoldState(folded = false)
foldUpdates.clear()
fireScreenOnEvent()
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE)
}
@Test
fun testSecondScreenOnEventWhenUnfolded_doesNotEmitEvents() {
setFoldState(folded = false)
fireScreenOnEvent()
foldUpdates.clear()
fireScreenOnEvent()
// No events as this is power button turn on
assertThat(foldUpdates).isEmpty()
}
@Test
fun testUnfoldedOpenedHingeAngleEmitted_isFinishedOpeningIsFalse() {
setFoldState(folded = false)
sendHingeAngleEvent(10)
assertThat(foldStateProvider.isFinishedOpening).isFalse()
}
@Test
fun testFoldedHalfOpenHingeAngleEmitted_isFinishedOpeningIsFalse() {
setFoldState(folded = true)
sendHingeAngleEvent(10)
assertThat(foldStateProvider.isFinishedOpening).isFalse()
}
@Test
fun testFoldedFullyOpenHingeAngleEmitted_isFinishedOpeningIsTrue() {
setFoldState(folded = false)
sendHingeAngleEvent(180)
assertThat(foldStateProvider.isFinishedOpening).isTrue()
}
@Test
fun testUnfoldedHalfOpenOpened_afterTimeout_isFinishedOpeningIsTrue() {
setFoldState(folded = false)
sendHingeAngleEvent(10)
simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS)
assertThat(foldStateProvider.isFinishedOpening).isTrue()
}
@Test
fun startClosingEvent_afterTimeout_abortEmitted() {
sendHingeAngleEvent(90)
sendHingeAngleEvent(80)
simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS)
assertThat(foldUpdates)
.containsExactly(FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_FINISH_HALF_OPEN)
}
@Test
fun startClosingEvent_beforeTimeout_abortNotEmitted() {
sendHingeAngleEvent(90)
sendHingeAngleEvent(80)
simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS - 1)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
}
@Test
fun startClosingEvent_eventBeforeTimeout_oneEventEmitted() {
sendHingeAngleEvent(180)
sendHingeAngleEvent(90)
simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS - 1)
sendHingeAngleEvent(80)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
}
@Test
fun startClosingEvent_timeoutAfterTimeoutRescheduled_abortEmitted() {
sendHingeAngleEvent(180)
sendHingeAngleEvent(90)
// The timeout should not trigger here.
simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS - 1)
sendHingeAngleEvent(80)
simulateTimeout(HALF_OPENED_TIMEOUT_MILLIS) // The timeout should trigger here.
assertThat(foldUpdates)
.containsExactly(FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_FINISH_HALF_OPEN)
}
@Test
fun startClosingEvent_shortTimeBetween_emitsOnlyOneEvents() {
sendHingeAngleEvent(180)
sendHingeAngleEvent(90)
sendHingeAngleEvent(80)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
}
@Test
fun startClosingEvent_whileClosing_emittedDespiteInitialAngle() {
val maxAngle = 180 - FULLY_OPEN_THRESHOLD_DEGREES.toInt()
for (i in 1..maxAngle) {
foldUpdates.clear()
simulateFolding(startAngle = i)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
simulateTimeout() // Timeout to set the state to aborted.
}
}
@Test
fun startClosingEvent_whileNotOnLauncher_doesNotTriggerBeforeThreshold() {
setupForegroundActivityType(isHomeActivity = false)
sendHingeAngleEvent(180)
sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
assertThat(foldUpdates).isEmpty()
}
@Test
fun startClosingEvent_whileActivityTypeNotAvailable_triggerBeforeThreshold() {
setupForegroundActivityType(isHomeActivity = null)
sendHingeAngleEvent(180)
sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
}
@Test
fun startClosingEvent_whileOnLauncher_doesTriggerBeforeThreshold() {
setupForegroundActivityType(isHomeActivity = true)
sendHingeAngleEvent(180)
sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
}
@Test
fun startClosingEvent_whileNotOnLauncher_triggersAfterThreshold() {
setupForegroundActivityType(isHomeActivity = false)
sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES)
sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES - 1)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
}
private fun setupForegroundActivityType(isHomeActivity: Boolean?) {
whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity)
}
private fun simulateTimeout(waitTime: Long = HALF_OPENED_TIMEOUT_MILLIS) {
val runnableDelay = scheduledRunnableDelay ?: throw Exception("No runnable scheduled.")
if (waitTime >= runnableDelay) {
scheduledRunnable?.run()
scheduledRunnable = null
scheduledRunnableDelay = null
}
}
private fun simulateFolding(startAngle: Int) {
sendHingeAngleEvent(startAngle)
sendHingeAngleEvent(startAngle - 1)
}
private fun setFoldState(folded: Boolean) {
foldProvider.notifyFolded(folded)
}
private fun fireScreenOnEvent() {
screenOnStatusProvider.notifyScreenTurnedOn()
}
private fun sendHingeAngleEvent(angle: Int) {
testHingeAngleProvider.notifyAngle(angle.toFloat())
}
private class TestFoldProvider : FoldProvider {
private val callbacks = arrayListOf<FoldCallback>()
override fun registerCallback(callback: FoldCallback, executor: Executor) {
callbacks += callback
}
override fun unregisterCallback(callback: FoldCallback) {
callbacks -= callback
}
fun notifyFolded(isFolded: Boolean) {
callbacks.forEach { it.onFoldUpdated(isFolded) }
}
}
private class TestScreenOnStatusProvider : ScreenStatusProvider {
private val callbacks = arrayListOf<ScreenListener>()
override fun addCallback(listener: ScreenListener) {
callbacks += listener
}
override fun removeCallback(listener: ScreenListener) {
callbacks -= listener
}
fun notifyScreenTurnedOn() {
callbacks.forEach { it.onScreenTurnedOn() }
}
}
private class TestHingeAngleProvider : HingeAngleProvider {
private val callbacks = arrayListOf<Consumer<Float>>()
var isStarted: Boolean = false
override fun start() {
isStarted = true;
}
override fun stop() {
isStarted = false;
}
override fun addCallback(listener: Consumer<Float>) {
callbacks += listener
}
override fun removeCallback(listener: Consumer<Float>) {
callbacks -= listener
}
fun notifyAngle(angle: Float) {
callbacks.forEach { it.accept(angle) }
}
}
companion object {
private const val HALF_OPENED_TIMEOUT_MILLIS = 300L
}
}