Cancel unfold animation when rotation changes

In DeviceFoldStateProvider.kt, FOLD_UPDATE_FINISH_HALF_OPEN is emitted if the rotation changes while the animation is in progress.

 This targets the case when the device goes to tabletop mode during the transition.

+ Using RotationChangeProvider to fix problems when the new orientation change was received in a background thread unexpectedly. This fixes: (1) wrong launcher icons movement after the rotation, and (2) wrong dark overlay scrim after rotation.

+ Minor formattig fixes by ktfmt

Bug: 241743859
Test: DeviceFoldStateProviderTest && NaturalRotationUnfoldProgressProviderTest && manual stress testing
Change-Id: I0ff6a823709e0891a7bcd13528a0e160d6f39c98
This commit is contained in:
Nicolo' Mazzucato
2022-10-12 13:40:40 +00:00
parent 683cfc81cc
commit 0def5f9805
10 changed files with 111 additions and 92 deletions

View File

@@ -17,6 +17,7 @@ package com.android.systemui.shared.animation
import android.graphics.Point
import android.view.Surface
import android.view.Surface.Rotation
import android.view.View
import android.view.WindowManager
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
@@ -58,14 +59,14 @@ class UnfoldMoveFromCenterAnimator @JvmOverloads constructor(
* Updates display properties in order to calculate the initial position for the views
* Must be called before [registerViewForAnimation]
*/
fun updateDisplayProperties() {
@JvmOverloads
fun updateDisplayProperties(@Rotation rotation: Int = windowManager.defaultDisplay.rotation) {
windowManager.defaultDisplay.getSize(screenSize)
// Simple implementation to get current fold orientation,
// this might not be correct on all devices
// TODO: use JetPack WindowManager library to get the fold orientation
isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 ||
windowManager.defaultDisplay.rotation == Surface.ROTATION_180
isVerticalFold = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
}
/**

View File

@@ -15,12 +15,11 @@
package com.android.systemui.unfold.util
import android.content.Context
import android.os.RemoteException
import android.view.IRotationWatcher
import android.view.IWindowManager
import android.view.Surface
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
/**
* [UnfoldTransitionProgressProvider] that emits transition progress only when the display has
@@ -29,27 +28,21 @@ import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionPr
*/
class NaturalRotationUnfoldProgressProvider(
private val context: Context,
private val windowManagerInterface: IWindowManager,
private val rotationChangeProvider: RotationChangeProvider,
unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider
) : UnfoldTransitionProgressProvider {
private val scopedUnfoldTransitionProgressProvider =
ScopedUnfoldTransitionProgressProvider(unfoldTransitionProgressProvider)
private val rotationWatcher = RotationWatcher()
private var isNaturalRotation: Boolean = false
fun init() {
try {
windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId)
} catch (e: RemoteException) {
throw e.rethrowFromSystemServer()
}
onRotationChanged(context.display.rotation)
rotationChangeProvider.addCallback(rotationListener)
rotationListener.onRotationChanged(context.display.rotation)
}
private fun onRotationChanged(rotation: Int) {
private val rotationListener = RotationListener { rotation ->
val isNewRotationNatural =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
@@ -60,12 +53,7 @@ class NaturalRotationUnfoldProgressProvider(
}
override fun destroy() {
try {
windowManagerInterface.removeRotationWatcher(rotationWatcher)
} catch (e: RemoteException) {
e.rethrowFromSystemServer()
}
rotationChangeProvider.removeCallback(rotationListener)
scopedUnfoldTransitionProgressProvider.destroy()
}
@@ -76,10 +64,4 @@ class NaturalRotationUnfoldProgressProvider(
override fun removeCallback(listener: TransitionProgressListener) {
scopedUnfoldTransitionProgressProvider.removeCallback(listener)
}
private inner class RotationWatcher : IRotationWatcher.Stub() {
override fun onRotationChanged(rotation: Int) {
this@NaturalRotationUnfoldProgressProvider.onRotationChanged(rotation)
}
}
}

View File

@@ -26,8 +26,6 @@ import android.os.Trace
import android.view.Choreographer
import android.view.Display
import android.view.DisplayInfo
import android.view.IRotationWatcher
import android.view.IWindowManager
import android.view.Surface
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
@@ -40,6 +38,7 @@ import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.util.traceSection
import com.android.wm.shell.displayareahelper.DisplayAreaHelper
import java.util.Optional
@@ -58,7 +57,7 @@ constructor(
private val displayAreaHelper: Optional<DisplayAreaHelper>,
@Main private val executor: Executor,
@UiBackground private val backgroundExecutor: Executor,
private val windowManagerInterface: IWindowManager
private val rotationChangeProvider: RotationChangeProvider,
) {
private val transitionListener = TransitionListener()
@@ -78,7 +77,7 @@ constructor(
fun init() {
deviceStateManager.registerCallback(executor, FoldListener())
unfoldTransitionProgressProvider.addCallback(transitionListener)
windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId)
rotationChangeProvider.addCallback(rotationWatcher)
val containerBuilder =
SurfaceControl.Builder(SurfaceSession())
@@ -86,7 +85,9 @@ constructor(
.setName("unfold-overlay-container")
displayAreaHelper.get().attachToRootDisplayArea(
Display.DEFAULT_DISPLAY, containerBuilder) { builder ->
Display.DEFAULT_DISPLAY,
containerBuilder
) { builder ->
executor.execute {
overlayContainer = builder.build()
@@ -244,8 +245,8 @@ constructor(
}
}
private inner class RotationWatcher : IRotationWatcher.Stub() {
override fun onRotationChanged(newRotation: Int) =
private inner class RotationWatcher : RotationChangeProvider.RotationListener {
override fun onRotationChanged(newRotation: Int) {
traceSection("UnfoldLightRevealOverlayAnimation#onRotationChanged") {
if (currentRotation != newRotation) {
currentRotation = newRotation
@@ -253,6 +254,7 @@ constructor(
root?.relayout(getLayoutParams())
}
}
}
}
private inner class FoldListener :

View File

@@ -17,11 +17,11 @@
package com.android.systemui.unfold
import android.content.Context
import android.view.IWindowManager
import com.android.systemui.keyguard.LifecycleScreenStatusProvider
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.system.SystemUnfoldSharedModule
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
@@ -65,11 +65,11 @@ class UnfoldTransitionModule {
@Singleton
fun provideNaturalRotationProgressProvider(
context: Context,
windowManager: IWindowManager,
rotationChangeProvider: RotationChangeProvider,
unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
): Optional<NaturalRotationUnfoldProgressProvider> =
unfoldTransitionProgressProvider.map { provider ->
NaturalRotationUnfoldProgressProvider(context, windowManager, provider)
NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, provider)
}
@Provides

View File

@@ -25,16 +25,21 @@ 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.RotationChangeProvider.RotationListener
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.android.systemui.util.mockito.capture
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -48,6 +53,12 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
@Mock
private lateinit var handler: Handler
@Mock
private lateinit var rotationChangeProvider: RotationChangeProvider
@Captor
private lateinit var rotationListener: ArgumentCaptor<RotationListener>
private val foldProvider = TestFoldProvider()
private val screenOnStatusProvider = TestScreenOnStatusProvider()
private val testHingeAngleProvider = TestHingeAngleProvider()
@@ -76,6 +87,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
screenOnStatusProvider,
foldProvider,
activityTypeProvider,
rotationChangeProvider,
context.mainExecutor,
handler
)
@@ -92,6 +104,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
})
foldStateProvider.start()
verify(rotationChangeProvider).addCallback(capture(rotationListener))
whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock ->
scheduledRunnable = invocationOnMock.getArgument<Runnable>(0)
scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1)
@@ -372,6 +386,27 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
assertThat(testHingeAngleProvider.isStarted).isFalse()
}
@Test
fun onRotationChanged_whileInProgress_cancelled() {
setFoldState(folded = false)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING)
rotationListener.value.onRotationChanged(1)
assertThat(foldUpdates).containsExactly(
FOLD_UPDATE_START_OPENING, FOLD_UPDATE_FINISH_HALF_OPEN)
}
@Test
fun onRotationChanged_whileNotInProgress_noUpdates() {
setFoldState(folded = true)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
rotationListener.value.onRotationChanged(1)
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
}
private fun setupForegroundActivityType(isHomeActivity: Boolean?) {
whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity)
}

View File

@@ -16,18 +16,19 @@
package com.android.systemui.unfold.util
import android.testing.AndroidTestingRunner
import android.view.IRotationWatcher
import android.view.IWindowManager
import android.view.Surface
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.TestUnfoldTransitionProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.util.mockito.any
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
import com.android.systemui.util.mockito.capture
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
@@ -38,32 +39,26 @@ import org.mockito.MockitoAnnotations
@SmallTest
class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
@Mock
lateinit var windowManager: IWindowManager
@Mock lateinit var rotationChangeProvider: RotationChangeProvider
private val sourceProvider = TestUnfoldTransitionProvider()
@Mock
lateinit var transitionListener: TransitionProgressListener
@Mock lateinit var transitionListener: TransitionProgressListener
@Captor private lateinit var rotationListenerCaptor: ArgumentCaptor<RotationListener>
lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
private val rotationWatcherCaptor =
ArgumentCaptor.forClass(IRotationWatcher.Stub::class.java)
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
progressProvider = NaturalRotationUnfoldProgressProvider(
context,
windowManager,
sourceProvider
)
progressProvider =
NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, sourceProvider)
progressProvider.init()
verify(windowManager).watchRotation(rotationWatcherCaptor.capture(), any())
verify(rotationChangeProvider).addCallback(capture(rotationListenerCaptor))
progressProvider.addCallback(transitionListener)
}
@@ -127,6 +122,6 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
}
private fun onRotationChanged(rotation: Int) {
rotationWatcherCaptor.value.onRotationChanged(rotation)
rotationListenerCaptor.value.onRotationChanged(rotation)
}
}

View File

@@ -20,10 +20,12 @@ import android.content.ContentResolver
import android.content.Context
import android.hardware.SensorManager
import android.os.Handler
import android.view.IWindowManager
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldBackground
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix
@@ -39,11 +41,11 @@ import javax.inject.Singleton
*
* This component is meant to be used for places that don't use dagger. By providing those
* parameters to the factory, all dagger objects are correctly instantiated. See
* [createUnfoldTransitionProgressProvider] for an example.
* [createUnfoldSharedComponent] for an example.
*/
@Singleton
@Component(modules = [UnfoldSharedModule::class])
internal interface UnfoldSharedComponent {
interface UnfoldSharedComponent {
@Component.Factory
interface Factory {
@@ -58,9 +60,11 @@ internal interface UnfoldSharedComponent {
@BindsInstance @UnfoldMain executor: Executor,
@BindsInstance @UnfoldBackground backgroundExecutor: Executor,
@BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
@BindsInstance windowManager: IWindowManager,
@BindsInstance contentResolver: ContentResolver = context.contentResolver
): UnfoldSharedComponent
}
val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider>
val rotationChangeProvider: RotationChangeProvider
}

View File

@@ -20,6 +20,7 @@ package com.android.systemui.unfold
import android.content.Context
import android.hardware.SensorManager
import android.os.Handler
import android.view.IWindowManager
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
@@ -27,14 +28,15 @@ import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import java.util.concurrent.Executor
/**
* Factory for [UnfoldTransitionProgressProvider].
* Factory for [UnfoldSharedComponent].
*
* This is needed as Launcher has to create the object manually. If dagger is available, this object
* is provided in [UnfoldSharedModule].
* This wraps the autogenerated factory (for discoverability), and is needed as Launcher has to
* create the object manually. If dagger is available, this object is provided in
* [UnfoldSharedModule].
*
* This should **never** be called from sysui, as the object is already provided in that process.
*/
fun createUnfoldTransitionProgressProvider(
fun createUnfoldSharedComponent(
context: Context,
config: UnfoldTransitionConfig,
screenStatusProvider: ScreenStatusProvider,
@@ -44,8 +46,9 @@ fun createUnfoldTransitionProgressProvider(
mainHandler: Handler,
mainExecutor: Executor,
backgroundExecutor: Executor,
tracingTagPrefix: String
): UnfoldTransitionProgressProvider =
tracingTagPrefix: String,
windowManager: IWindowManager,
): UnfoldSharedComponent =
DaggerUnfoldSharedComponent.factory()
.create(
context,
@@ -57,9 +60,6 @@ fun createUnfoldTransitionProgressProvider(
mainHandler,
mainExecutor,
backgroundExecutor,
tracingTagPrefix)
.unfoldTransitionProvider
.orElse(null)
?: throw IllegalStateException(
"Trying to create " +
"UnfoldTransitionProgressProvider when the transition is disabled")
tracingTagPrefix,
windowManager,
)

View File

@@ -26,7 +26,8 @@ import com.android.systemui.unfold.util.CallbackController
*
* onTransitionProgress callback could be called on each frame.
*
* Use [createUnfoldTransitionProgressProvider] to create instances of this interface
* Use [createUnfoldSharedComponent] to create instances of this interface when dagger is not
* available.
*/
interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgressListener> {

View File

@@ -24,6 +24,7 @@ import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES
import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
@@ -40,22 +41,24 @@ constructor(
private val screenStatusProvider: ScreenStatusProvider,
private val foldProvider: FoldProvider,
private val activityTypeProvider: CurrentActivityTypeProvider,
private val rotationChangeProvider: RotationChangeProvider,
@UnfoldMain private val mainExecutor: Executor,
@UnfoldMain private val handler: Handler
) : FoldStateProvider {
private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
@FoldUpdate
private var lastFoldUpdate: Int? = null
@FoldUpdate private var lastFoldUpdate: Int? = null
@FloatRange(from = 0.0, to = 180.0)
private var lastHingeAngle: Float = 0f
@FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f
private val hingeAngleListener = HingeAngleListener()
private val screenListener = ScreenStatusListener()
private val foldStateListener = FoldStateListener()
private val timeoutRunnable = TimeoutRunnable()
private val timeoutRunnable = Runnable { cancelAnimation() }
private val rotationListener = RotationListener {
if (isTransitionInProgress) cancelAnimation()
}
/**
* Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
@@ -72,6 +75,7 @@ constructor(
foldProvider.registerCallback(foldStateListener, mainExecutor)
screenStatusProvider.addCallback(screenListener)
hingeAngleProvider.addCallback(hingeAngleListener)
rotationChangeProvider.addCallback(rotationListener)
}
override fun stop() {
@@ -79,6 +83,7 @@ constructor(
foldProvider.unregisterCallback(foldStateListener)
hingeAngleProvider.removeCallback(hingeAngleListener)
hingeAngleProvider.stop()
rotationChangeProvider.removeCallback(rotationListener)
}
override fun addCallback(listener: FoldUpdatesListener) {
@@ -90,14 +95,15 @@ constructor(
}
override val isFinishedOpening: Boolean
get() = !isFolded &&
get() =
!isFolded &&
(lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN ||
lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
private val isTransitionInProgress: Boolean
get() =
lastFoldUpdate == FOLD_UPDATE_START_OPENING ||
lastFoldUpdate == FOLD_UPDATE_START_CLOSING
lastFoldUpdate == FOLD_UPDATE_START_CLOSING
private fun onHingeAngle(angle: Float) {
if (DEBUG) {
@@ -168,7 +174,7 @@ constructor(
private fun notifyFoldUpdate(@FoldUpdate update: Int) {
if (DEBUG) {
Log.d(TAG, stateToString(update))
Log.d(TAG, update.name())
}
outputListeners.forEach { it.onFoldUpdate(update) }
lastFoldUpdate = update
@@ -185,6 +191,8 @@ constructor(
handler.removeCallbacks(timeoutRunnable)
}
private fun cancelAnimation(): Unit = notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
override fun onScreenTurnedOn() {
@@ -225,16 +233,10 @@ constructor(
onHingeAngle(angle)
}
}
private inner class TimeoutRunnable : Runnable {
override fun run() {
notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
}
}
}
private fun stateToString(@FoldUpdate update: Int): String {
return when (update) {
fun @receiver:FoldUpdate Int.name() =
when (this) {
FOLD_UPDATE_START_OPENING -> "START_OPENING"
FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE"
@@ -243,15 +245,12 @@ private fun stateToString(@FoldUpdate update: Int): String {
FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
else -> "UNKNOWN"
}
}
private const val TAG = "DeviceFoldProvider"
private const val DEBUG = false
/** Threshold after which we consider the device fully unfolded. */
@VisibleForTesting
const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
@VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
/** Fold animation on top of apps only when the angle exceeds this threshold. */
@VisibleForTesting
const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
@VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60