| /* |
| * 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 |
| } |
| } |