Loading displaylib/src/com/android/app/displaylib/DisplayLibComponent.kt +1 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ interface DisplayLibComponent { val displayRepository: DisplayRepository val displaysWithDecorationsRepository: DisplaysWithDecorationsRepository val displaysWithDecorationsRepositoryCompat: DisplaysWithDecorationsRepositoryCompat } @Module Loading displaylib/src/com/android/app/displaylib/DisplaysWithDecorationsRepositoryCompat.kt 0 → 100644 +129 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.app.displaylib import com.android.internal.annotations.GuardedBy import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext /** Listener for display system decorations changes. */ interface DisplayDecorationListener { /** Called when system decorations should be added to the display.* */ fun onDisplayAddSystemDecorations(displayId: Int) /** Called when a display is removed. */ fun onDisplayRemoved(displayId: Int) /** Called when system decorations should be removed from the display. */ fun onDisplayRemoveSystemDecorations(displayId: Int) } /** * This class is a compatibility layer that allows to register and unregister listeners for display * decorations changes. It uses a [DisplaysWithDecorationsRepository] to get the current list of * displays with decorations and notifies the listeners when the list changes. */ @Singleton class DisplaysWithDecorationsRepositoryCompat @Inject constructor( private val bgApplicationScope: CoroutineScope, private val displayRepository: DisplaysWithDecorationsRepository, ) { private val mutex = Mutex() private var collectorJob: Job? = null private val displayDecorationListenersWithDispatcher = ConcurrentHashMap<DisplayDecorationListener, CoroutineDispatcher>() /** * Registers a [DisplayDecorationListener] to be notified when the list of displays with * decorations changes. * * @param listener The listener to register. * @param dispatcher The dispatcher to use when notifying the listener. */ fun registerDisplayDecorationListener( listener: DisplayDecorationListener, dispatcher: CoroutineDispatcher, ) { bgApplicationScope.launch { mutex.withLock { displayDecorationListenersWithDispatcher[listener] = dispatcher // Emit all the existing displays with decorations when registering. displayRepository.displayIdsWithSystemDecorations.value.forEach { displayId -> withContext(dispatcher) { listener.onDisplayAddSystemDecorations(displayId) } } startCollectingIfNeeded() } } } /** * Unregisters a [DisplayDecorationListener]. * * @param listener The listener to unregister. */ fun unregisterDisplayDecorationListener(listener: DisplayDecorationListener) { bgApplicationScope.launch { mutex.withLock { displayDecorationListenersWithDispatcher.remove(listener) // stop collecting if no listeners if (displayDecorationListenersWithDispatcher.isEmpty()) { collectorJob?.cancel() collectorJob = null } } } } @GuardedBy("mutex") private fun startCollectingIfNeeded() { if (collectorJob?.isActive == true) { return } var oldDisplays: Set<Int> = displayRepository.displayIdsWithSystemDecorations.value collectorJob = bgApplicationScope.launch { displayRepository.displayIdsWithSystemDecorations.collect { currentDisplays -> val previous = oldDisplays oldDisplays = currentDisplays val newDisplaysWithDecorations = currentDisplays - previous val removedDisplays = previous - currentDisplays displayDecorationListenersWithDispatcher.forEach { (listener, dispatcher) -> withContext(dispatcher) { newDisplaysWithDecorations.forEach { displayId -> listener.onDisplayAddSystemDecorations(displayId) } removedDisplays.forEach { displayId -> listener.onDisplayRemoveSystemDecorations(displayId) } } } } } } } mechanics/src/com/android/mechanics/impl/Computations.kt +28 −17 Original line number Diff line number Diff line Loading @@ -435,7 +435,7 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static var guaranteeState = lastGuaranteeState var springState = lastSpringState var springParameters = lastAnimation.springParameters var hasJumped = false var initialSpringVelocity = directMappedVelocity var segmentIndex = sourceIndex while (segmentIndex != targetIndex) { Loading Loading @@ -481,6 +481,13 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static guaranteeState.updatedSpringParameters(lastBreakpoint) } springState = springState.calculateUpdatedState( nextBreakpointCrossTime - lastAnimationTime, springParameters, ) lastAnimationTime = nextBreakpointCrossTime val mappingBefore = mappings[segmentIndex] val beforeBreakpoint = mappingBefore.map(nextBreakpoint.position) val mappingAfter = mappings[segmentIndex + directionOffset] Loading @@ -488,7 +495,26 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static val delta = afterBreakpoint - beforeBreakpoint val deltaIsFinite = delta.fastIsFinite() if (!deltaIsFinite) { if (deltaIsFinite && delta != 0f) { // There is a discontinuity on this breakpoint, that needs to be // animated. The delta is pushed to the spring, to consume the // discontinuity over time. springState = springState.nudge( displacementDelta = -delta, velocityDelta = initialSpringVelocity, ) // When *first* crossing a discontinuity in a given frame, the static // mapped velocity observed during previous frame is added as initial // velocity to the spring. This is done ot most once per frame, and only // if there is an actual discontinuity. initialSpringVelocity = 0f } else { // The before and / or after mapping produced an non-finite number, // which is not allowed. This intentionally crashes eng-builds, since // it's a bug in the Mapping implementation that must be fixed. On // regular builds, it will likely cause a jumpcut. Log.wtf( TAG, "Delta between breakpoints is undefined!\n" + Loading @@ -499,21 +525,6 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static ) } if (!hasJumped && delta != 0f) { hasJumped = true springState = springState.nudge(velocityDelta = directMappedVelocity) } springState = springState.calculateUpdatedState( nextBreakpointCrossTime - lastAnimationTime, springParameters, ) lastAnimationTime = nextBreakpointCrossTime if (deltaIsFinite) { springState = springState.nudge(displacementDelta = -delta) } segmentIndex += directionOffset lastBreakpoint = nextBreakpoint guaranteeState = Loading mechanics/tests/goldens/MagneticDetach/placedAfter_afterAttach_detachesAgain.json +39 −39 Original line number Diff line number Diff line Loading @@ -214,25 +214,25 @@ 65, 60, 55, 46.603424, 42.153717, 36.203827, 30.321402, 25.1608, 21.651463, 18.9976, 17.06427, 15.6970415, 14.754084, 14.11806, 13.697852, 13.425745, 13.253073, 13.145809, 13.080716, 13.042264, 13.020281, 13.008238, 50, 43.38443, 36.351646, 29.990938, 24.672552, 21.162388, 18.574236, 16.725906, 15.440355, 14.566638, 13.985239, 13.6060915, 13.363756, 13.212058, 13.11921, 13.063812, 13.031747, 13.013887, 13.004453, 13, 13.75, 14.5, Loading @@ -245,26 +245,26 @@ 28.15, 30.1, 32.05, 35.264374, 44.949898, 58.693554, 67.97366, 76.22729, 82.931595, 88.0746, 91.862114, 94.56434, 96.44223, 97.71758, 98.56564, 99.118324, 99.47132, 99.69214, 99.82718, 99.90767, 99.954216, 99.980095, 99.99374, 34, 44.585567, 58.759357, 68.21262, 76.507256, 83.19111, 88.2904, 92.03026, 94.689606, 96.532425, 97.780754, 98.60885, 99.14723, 99.49028, 99.70432, 99.83485, 99.9124, 99.957054, 99.98176, 99.994675, 100 ] }, Loading mechanics/tests/goldens/MagneticDetach/placedAfter_attach_snapsToOrigin.json +24 −24 Original line number Diff line number Diff line Loading @@ -133,30 +133,30 @@ 65, 60, 55, 46.603424, 42.153717, 36.203827, 30.321402, 25.1608, 20.901463, 17.4976, 14.814271, 12.6970415, 6.739443, 1.0775535, 0.635472, 0.35061052, 0.17432979, 0.07051067, 0.013341078, -0.014990943, -0.02636234, -0.028412364, -0.025858387, -0.02147111, -0.016770272, -0.012503948, -0.008967604, 50, 43.38443, 36.351646, 29.990938, 24.672552, 20.412388, 17.074236, 14.475905, 12.440355, 6.552413, 0.9461464, 0.54626375, 0.29212147, 0.13740596, 0.048214816, 0.0006277391, -0.021660766, -0.02938723, -0.029362231, -0.02572238, -0.020845085, -0.015992891, -0.01175198, -0.008320414, 0 ] }, Loading Loading
displaylib/src/com/android/app/displaylib/DisplayLibComponent.kt +1 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ interface DisplayLibComponent { val displayRepository: DisplayRepository val displaysWithDecorationsRepository: DisplaysWithDecorationsRepository val displaysWithDecorationsRepositoryCompat: DisplaysWithDecorationsRepositoryCompat } @Module Loading
displaylib/src/com/android/app/displaylib/DisplaysWithDecorationsRepositoryCompat.kt 0 → 100644 +129 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.app.displaylib import com.android.internal.annotations.GuardedBy import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext /** Listener for display system decorations changes. */ interface DisplayDecorationListener { /** Called when system decorations should be added to the display.* */ fun onDisplayAddSystemDecorations(displayId: Int) /** Called when a display is removed. */ fun onDisplayRemoved(displayId: Int) /** Called when system decorations should be removed from the display. */ fun onDisplayRemoveSystemDecorations(displayId: Int) } /** * This class is a compatibility layer that allows to register and unregister listeners for display * decorations changes. It uses a [DisplaysWithDecorationsRepository] to get the current list of * displays with decorations and notifies the listeners when the list changes. */ @Singleton class DisplaysWithDecorationsRepositoryCompat @Inject constructor( private val bgApplicationScope: CoroutineScope, private val displayRepository: DisplaysWithDecorationsRepository, ) { private val mutex = Mutex() private var collectorJob: Job? = null private val displayDecorationListenersWithDispatcher = ConcurrentHashMap<DisplayDecorationListener, CoroutineDispatcher>() /** * Registers a [DisplayDecorationListener] to be notified when the list of displays with * decorations changes. * * @param listener The listener to register. * @param dispatcher The dispatcher to use when notifying the listener. */ fun registerDisplayDecorationListener( listener: DisplayDecorationListener, dispatcher: CoroutineDispatcher, ) { bgApplicationScope.launch { mutex.withLock { displayDecorationListenersWithDispatcher[listener] = dispatcher // Emit all the existing displays with decorations when registering. displayRepository.displayIdsWithSystemDecorations.value.forEach { displayId -> withContext(dispatcher) { listener.onDisplayAddSystemDecorations(displayId) } } startCollectingIfNeeded() } } } /** * Unregisters a [DisplayDecorationListener]. * * @param listener The listener to unregister. */ fun unregisterDisplayDecorationListener(listener: DisplayDecorationListener) { bgApplicationScope.launch { mutex.withLock { displayDecorationListenersWithDispatcher.remove(listener) // stop collecting if no listeners if (displayDecorationListenersWithDispatcher.isEmpty()) { collectorJob?.cancel() collectorJob = null } } } } @GuardedBy("mutex") private fun startCollectingIfNeeded() { if (collectorJob?.isActive == true) { return } var oldDisplays: Set<Int> = displayRepository.displayIdsWithSystemDecorations.value collectorJob = bgApplicationScope.launch { displayRepository.displayIdsWithSystemDecorations.collect { currentDisplays -> val previous = oldDisplays oldDisplays = currentDisplays val newDisplaysWithDecorations = currentDisplays - previous val removedDisplays = previous - currentDisplays displayDecorationListenersWithDispatcher.forEach { (listener, dispatcher) -> withContext(dispatcher) { newDisplaysWithDecorations.forEach { displayId -> listener.onDisplayAddSystemDecorations(displayId) } removedDisplays.forEach { displayId -> listener.onDisplayRemoveSystemDecorations(displayId) } } } } } } }
mechanics/src/com/android/mechanics/impl/Computations.kt +28 −17 Original line number Diff line number Diff line Loading @@ -435,7 +435,7 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static var guaranteeState = lastGuaranteeState var springState = lastSpringState var springParameters = lastAnimation.springParameters var hasJumped = false var initialSpringVelocity = directMappedVelocity var segmentIndex = sourceIndex while (segmentIndex != targetIndex) { Loading Loading @@ -481,6 +481,13 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static guaranteeState.updatedSpringParameters(lastBreakpoint) } springState = springState.calculateUpdatedState( nextBreakpointCrossTime - lastAnimationTime, springParameters, ) lastAnimationTime = nextBreakpointCrossTime val mappingBefore = mappings[segmentIndex] val beforeBreakpoint = mappingBefore.map(nextBreakpoint.position) val mappingAfter = mappings[segmentIndex + directionOffset] Loading @@ -488,7 +495,26 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static val delta = afterBreakpoint - beforeBreakpoint val deltaIsFinite = delta.fastIsFinite() if (!deltaIsFinite) { if (deltaIsFinite && delta != 0f) { // There is a discontinuity on this breakpoint, that needs to be // animated. The delta is pushed to the spring, to consume the // discontinuity over time. springState = springState.nudge( displacementDelta = -delta, velocityDelta = initialSpringVelocity, ) // When *first* crossing a discontinuity in a given frame, the static // mapped velocity observed during previous frame is added as initial // velocity to the spring. This is done ot most once per frame, and only // if there is an actual discontinuity. initialSpringVelocity = 0f } else { // The before and / or after mapping produced an non-finite number, // which is not allowed. This intentionally crashes eng-builds, since // it's a bug in the Mapping implementation that must be fixed. On // regular builds, it will likely cause a jumpcut. Log.wtf( TAG, "Delta between breakpoints is undefined!\n" + Loading @@ -499,21 +525,6 @@ internal abstract class Computations : CurrentFrameInput, LastFrameState, Static ) } if (!hasJumped && delta != 0f) { hasJumped = true springState = springState.nudge(velocityDelta = directMappedVelocity) } springState = springState.calculateUpdatedState( nextBreakpointCrossTime - lastAnimationTime, springParameters, ) lastAnimationTime = nextBreakpointCrossTime if (deltaIsFinite) { springState = springState.nudge(displacementDelta = -delta) } segmentIndex += directionOffset lastBreakpoint = nextBreakpoint guaranteeState = Loading
mechanics/tests/goldens/MagneticDetach/placedAfter_afterAttach_detachesAgain.json +39 −39 Original line number Diff line number Diff line Loading @@ -214,25 +214,25 @@ 65, 60, 55, 46.603424, 42.153717, 36.203827, 30.321402, 25.1608, 21.651463, 18.9976, 17.06427, 15.6970415, 14.754084, 14.11806, 13.697852, 13.425745, 13.253073, 13.145809, 13.080716, 13.042264, 13.020281, 13.008238, 50, 43.38443, 36.351646, 29.990938, 24.672552, 21.162388, 18.574236, 16.725906, 15.440355, 14.566638, 13.985239, 13.6060915, 13.363756, 13.212058, 13.11921, 13.063812, 13.031747, 13.013887, 13.004453, 13, 13.75, 14.5, Loading @@ -245,26 +245,26 @@ 28.15, 30.1, 32.05, 35.264374, 44.949898, 58.693554, 67.97366, 76.22729, 82.931595, 88.0746, 91.862114, 94.56434, 96.44223, 97.71758, 98.56564, 99.118324, 99.47132, 99.69214, 99.82718, 99.90767, 99.954216, 99.980095, 99.99374, 34, 44.585567, 58.759357, 68.21262, 76.507256, 83.19111, 88.2904, 92.03026, 94.689606, 96.532425, 97.780754, 98.60885, 99.14723, 99.49028, 99.70432, 99.83485, 99.9124, 99.957054, 99.98176, 99.994675, 100 ] }, Loading
mechanics/tests/goldens/MagneticDetach/placedAfter_attach_snapsToOrigin.json +24 −24 Original line number Diff line number Diff line Loading @@ -133,30 +133,30 @@ 65, 60, 55, 46.603424, 42.153717, 36.203827, 30.321402, 25.1608, 20.901463, 17.4976, 14.814271, 12.6970415, 6.739443, 1.0775535, 0.635472, 0.35061052, 0.17432979, 0.07051067, 0.013341078, -0.014990943, -0.02636234, -0.028412364, -0.025858387, -0.02147111, -0.016770272, -0.012503948, -0.008967604, 50, 43.38443, 36.351646, 29.990938, 24.672552, 20.412388, 17.074236, 14.475905, 12.440355, 6.552413, 0.9461464, 0.54626375, 0.29212147, 0.13740596, 0.048214816, 0.0006277391, -0.021660766, -0.02938723, -0.029362231, -0.02572238, -0.020845085, -0.015992891, -0.01175198, -0.008320414, 0 ] }, Loading