Extract unfold classes to a separate library

No behaviour changes in this CL.
Extracted classes related to the fold/unfold
animation (that plays on foldable devices) to
a separate library that does not depend on
system classes. This is necessary to ease
reusing these classes in live wallpapers.

Bug: 231936088
Test: fold/unfold device
Change-Id: Ic1e1ec742867fca9c1209b930accd5738de18e39
This commit is contained in:
Nick Chameyev
2022-05-11 14:49:35 +01:00
parent e9ed7210ad
commit 0f3c4e522e
44 changed files with 886 additions and 283 deletions

View File

@@ -47,6 +47,7 @@ android_library {
],
static_libs: [
"PluginCoreLib",
"SystemUIUnfoldLib",
"androidx.dynamicanimation_dynamicanimation",
"androidx.concurrent_concurrent-futures",
"dagger2",

View File

@@ -1,49 +0,0 @@
/*
* 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.config
import android.content.Context
import android.os.SystemProperties
internal class ResourceUnfoldTransitionConfig(private val context: Context) :
UnfoldTransitionConfig {
override val isEnabled: Boolean
get() = readIsEnabledResource() && isPropertyEnabled
override val isHingeAngleEnabled: Boolean
get() = readIsHingeAngleEnabled()
private val isPropertyEnabled: Boolean
get() =
SystemProperties.getInt(
UNFOLD_TRANSITION_MODE_PROPERTY_NAME, UNFOLD_TRANSITION_PROPERTY_ENABLED) ==
UNFOLD_TRANSITION_PROPERTY_ENABLED
private fun readIsEnabledResource(): Boolean =
context.resources.getBoolean(com.android.internal.R.bool.config_unfoldTransitionEnabled)
private fun readIsHingeAngleEnabled(): Boolean =
context.resources.getBoolean(com.android.internal.R.bool.config_unfoldTransitionHingeAngle)
}
/**
* Temporary persistent property to control unfold transition mode.
*
* See [com.android.unfold.config.AnimationMode].
*/
private const val UNFOLD_TRANSITION_MODE_PROPERTY_NAME = "persist.unfold.transition_enabled"
private const val UNFOLD_TRANSITION_PROPERTY_ENABLED = 1

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2022 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.system
import android.app.ActivityManager
import android.app.WindowConfiguration
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ActivityManagerActivityTypeProvider @Inject constructor(
private val activityManager: ActivityManager
) : CurrentActivityTypeProvider {
override val isHomeActivity: Boolean?
get() {
val activityType = activityManager.getRunningTasks(/* maxNum= */ 1)
?.getOrNull(0)?.topActivityType ?: return null
return activityType == WindowConfiguration.ACTIVITY_TYPE_HOME
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2022 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.system
import android.content.Context
import android.hardware.devicestate.DeviceStateManager
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DeviceStateManagerFoldProvider @Inject constructor(
private val deviceStateManager: DeviceStateManager,
private val context: Context
) : FoldProvider {
private val callbacks: MutableMap<FoldCallback,
DeviceStateManager.DeviceStateCallback> = hashMapOf()
override fun registerCallback(callback: FoldCallback, executor: Executor) {
val listener = FoldStateListener(context, callback)
deviceStateManager.registerCallback(executor, listener)
callbacks[callback] = listener
}
override fun unregisterCallback(callback: FoldCallback) {
val listener = callbacks.remove(callback)
listener?.let {
deviceStateManager.unregisterCallback(it)
}
}
private inner class FoldStateListener(
context: Context,
listener: FoldCallback
) : DeviceStateManager.FoldStateListener(context, { listener.onFoldUpdated(it) })
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2022 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.system
import android.os.Handler
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
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.util.CurrentActivityTypeProvider
import dagger.Binds
import dagger.Module
import java.util.concurrent.Executor
/**
* Dagger module with system-only dependencies for the unfold animation.
* The code that is used to calculate unfold transition progress
* depends on some hidden APIs that are not available in normal
* apps. In order to re-use this code and use alternative implementations
* of these classes in other apps and hidden APIs here.
*/
@Module
abstract class SystemUnfoldSharedModule {
@Binds
abstract fun activityTypeProvider(executor: ActivityManagerActivityTypeProvider):
CurrentActivityTypeProvider
@Binds
abstract fun config(config: ResourceUnfoldTransitionConfig): UnfoldTransitionConfig
@Binds
abstract fun foldState(provider: DeviceStateManagerFoldProvider): FoldProvider
@Binds
@UnfoldMain
abstract fun mainExecutor(@Main executor: Executor): Executor
@Binds
@UnfoldMain
abstract fun mainHandler(@Main handler: Handler): Handler
@Binds
@UnfoldBackground
abstract fun backgroundExecutor(@UiBackground executor: Executor): Executor
}

View File

@@ -1,13 +0,0 @@
package com.android.systemui.unfold.updates.hinge
import androidx.core.util.Consumer
internal object EmptyHingeAngleProvider : HingeAngleProvider {
override fun start() {}
override fun stop() {}
override fun removeCallback(listener: Consumer<Float>) {}
override fun addCallback(listener: Consumer<Float>) {}
}

View File

@@ -1,19 +0,0 @@
package com.android.systemui.unfold.updates.hinge
import androidx.core.util.Consumer
import com.android.systemui.statusbar.policy.CallbackController
/**
* Emits device hinge angle values (angle between two integral parts of the device).
*
* The hinge angle could be from 0 to 360 degrees inclusive. For foldable devices usually 0
* corresponds to fully closed (folded) state and 180 degrees corresponds to fully open (flat)
* state.
*/
interface HingeAngleProvider : CallbackController<Consumer<Float>> {
fun start()
fun stop()
}
const val FULLY_OPEN_DEGREES = 180f
const val FULLY_CLOSED_DEGREES = 0f

View File

@@ -1,3 +1,17 @@
/*
* Copyright (C) 2022 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.util
import android.content.Context

View File

@@ -20,6 +20,7 @@ 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.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
@@ -34,7 +35,7 @@ import java.util.Optional
import javax.inject.Named
import javax.inject.Singleton
@Module(includes = [UnfoldSharedModule::class])
@Module(includes = [UnfoldSharedModule::class, SystemUnfoldSharedModule::class])
class UnfoldTransitionModule {
@Provides @UnfoldTransitionATracePrefix fun tracingTagPrefix() = "systemui"
@@ -60,11 +61,6 @@ class UnfoldTransitionModule {
FoldStateLogger(FoldStateLoggingProvider)
}
@Provides
@Singleton
fun provideUnfoldTransitionConfig(context: Context): UnfoldTransitionConfig =
createConfig(context)
@Provides
@Singleton
fun provideNaturalRotationProgressProvider(

View File

@@ -1,3 +1,17 @@
/*
* Copyright (C) 2022 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.shared.animation
import android.testing.AndroidTestingRunner
@@ -7,31 +21,24 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.TestUnfoldTransitionProvider
import org.junit.Assert.assertEquals
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
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() {
@Mock private lateinit var progressProvider: UnfoldTransitionProgressProvider
private val progressProvider = TestUnfoldTransitionProvider()
@Mock private lateinit var parent: ViewGroup
@Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener>
private lateinit var animator: UnfoldConstantTranslateAnimator
private lateinit var progressListener: TransitionProgressListener
private val viewsIdToRegister =
setOf(
@@ -46,17 +53,14 @@ class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() {
UnfoldConstantTranslateAnimator(viewsIdToRegister, progressProvider)
animator.init(parent, MAX_TRANSLATION)
verify(progressProvider).addCallback(progressListenerCaptor.capture())
progressListener = progressListenerCaptor.value
}
@Test
fun onTransition_noMatchingIds() {
// GIVEN no views matching any ids
// WHEN the transition starts
progressListener.onTransitionStarted()
progressListener.onTransitionProgress(.1f)
progressProvider.onTransitionStarted()
progressProvider.onTransitionProgress(.1f)
// THEN nothing... no exceptions
}
@@ -86,22 +90,22 @@ class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() {
// Compare values as ints because -0f != 0f
// WHEN the transition starts
progressListener.onTransitionStarted()
progressListener.onTransitionProgress(0f)
progressProvider.onTransitionStarted()
progressProvider.onTransitionProgress(0f)
list.forEach { (view, direction) ->
assertEquals((-MAX_TRANSLATION * direction).toInt(), view.translationX.toInt())
}
// WHEN the transition progresses, translation is updated
progressListener.onTransitionProgress(.5f)
progressProvider.onTransitionProgress(.5f)
list.forEach { (view, direction) ->
assertEquals((-MAX_TRANSLATION / 2f * direction).toInt(), view.translationX.toInt())
}
// WHEN the transition ends, translation is completed
progressListener.onTransitionProgress(1f)
progressListener.onTransitionFinished()
progressProvider.onTransitionProgress(1f)
progressProvider.onTransitionFinished()
list.forEach { (view, _) -> assertEquals(0, view.translationX.toInt()) }
}

View File

@@ -136,6 +136,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
private class UnfoldConfig : UnfoldTransitionConfig {
override var isEnabled: Boolean = false
override var isHingeAngleEnabled: Boolean = false
override val halfFoldedTimeoutMillis: Int = 0
}
private class TestTouchEventHandler : PhoneStatusBarView.TouchEventHandler {

View File

@@ -25,28 +25,20 @@ import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.util.TestFoldStateProvider
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
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.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class FoldStateLoggingProviderTest : SysuiTestCase() {
@Captor
private lateinit var foldUpdatesListener: ArgumentCaptor<FoldStateProvider.FoldUpdatesListener>
@Mock private lateinit var foldStateProvider: FoldStateProvider
private val testFoldStateProvider = TestFoldStateProvider()
private val fakeClock = FakeSystemClock()
private lateinit var foldStateLoggingProvider: FoldStateLoggingProvider
@@ -65,12 +57,10 @@ class FoldStateLoggingProviderTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
foldStateLoggingProvider =
FoldStateLoggingProviderImpl(foldStateProvider, fakeClock).apply {
FoldStateLoggingProviderImpl(testFoldStateProvider, fakeClock).apply {
addCallback(foldStateLoggingListener)
init()
}
verify(foldStateProvider).addCallback(foldUpdatesListener.capture())
}
@Test
@@ -183,10 +173,10 @@ class FoldStateLoggingProviderTest : SysuiTestCase() {
fun uninit_removesCallback() {
foldStateLoggingProvider.uninit()
verify(foldStateProvider).removeCallback(foldUpdatesListener.value)
assertThat(testFoldStateProvider.hasListeners).isFalse()
}
private fun sendFoldUpdate(@FoldUpdate foldUpdate: Int) {
foldUpdatesListener.value.onFoldUpdate(foldUpdate)
testFoldStateProvider.sendFoldUpdate(foldUpdate)
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2022 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.config
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
/**
* A test that checks that we load correct resources in
* ResourceUnfoldTransitionConfig as we use strings there instead of R constants.
* Internal Android resource constants are not available in public APIs,
* so we can't use them there directly.
*/
@RunWith(AndroidTestingRunner::class)
@SmallTest
class ResourceUnfoldTransitionConfigTest : SysuiTestCase() {
private val config = ResourceUnfoldTransitionConfig()
@Test
fun testIsEnabled() {
assertThat(config.isEnabled).isEqualTo(mContext.resources
.getBoolean(com.android.internal.R.bool.config_unfoldTransitionEnabled))
}
@Test
fun testHingeAngleEnabled() {
assertThat(config.isHingeAngleEnabled).isEqualTo(mContext.resources
.getBoolean(com.android.internal.R.bool.config_unfoldTransitionHingeAngle))
}
@Test
fun testHalfFoldedTimeout() {
assertThat(config.halfFoldedTimeoutMillis).isEqualTo(mContext.resources
.getInteger(com.android.internal.R.integer.config_unfoldTransitionHalfFoldedTimeout))
}
}

View File

@@ -16,82 +16,69 @@
package com.android.systemui.unfold.updates
import android.app.ActivityManager
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.ActivityType
import android.hardware.devicestate.DeviceStateManager
import android.hardware.devicestate.DeviceStateManager.FoldStateListener
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.unfold.util.FoldableDeviceStates
import com.android.systemui.unfold.util.FoldableTestUtils
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.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
import java.util.concurrent.Executor
import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidTestingRunner::class)
@SmallTest
class DeviceFoldStateProviderTest : SysuiTestCase() {
@Mock private lateinit var hingeAngleProvider: HingeAngleProvider
@Mock
private lateinit var activityTypeProvider: ActivityManagerActivityTypeProvider
@Mock private lateinit var screenStatusProvider: ScreenStatusProvider
@Mock
private lateinit var handler: Handler
@Mock private lateinit var deviceStateManager: DeviceStateManager
@Mock private lateinit var activityManager: ActivityManager
@Mock private lateinit var handler: Handler
@Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener>
@Captor private lateinit var screenOnListenerCaptor: ArgumentCaptor<ScreenListener>
@Captor private lateinit var hingeAngleCaptor: ArgumentCaptor<Consumer<Float>>
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 lateinit var deviceStates: FoldableDeviceStates
private var scheduledRunnable: Runnable? = null
private var scheduledRunnableDelay: Long? = null
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
overrideResource(
com.android.internal.R.integer.config_unfoldTransitionHalfFoldedTimeout,
HALF_OPENED_TIMEOUT_MILLIS.toInt())
deviceStates = FoldableTestUtils.findDeviceStates(context)
val config = object : UnfoldTransitionConfig by ResourceUnfoldTransitionConfig() {
override val halfFoldedTimeoutMillis: Int
get() = HALF_OPENED_TIMEOUT_MILLIS.toInt()
}
foldStateProvider =
DeviceFoldStateProvider(
context,
hingeAngleProvider,
screenStatusProvider,
deviceStateManager,
activityManager,
config,
testHingeAngleProvider,
screenOnStatusProvider,
foldProvider,
activityTypeProvider,
context.mainExecutor,
handler)
handler
)
foldStateProvider.addCallback(
object : FoldStateProvider.FoldUpdatesListener {
@@ -105,10 +92,6 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
})
foldStateProvider.start()
verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture())
verify(screenStatusProvider).addCallback(screenOnListenerCaptor.capture())
verify(hingeAngleProvider).addCallback(hingeAngleCaptor.capture())
whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock ->
scheduledRunnable = invocationOnMock.getArgument<Runnable>(0)
scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1)
@@ -125,7 +108,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
}
// By default, we're on launcher.
setupForegroundActivityType(ACTIVITY_TYPE_HOME)
setupForegroundActivityType(isHomeActivity = true)
}
@Test
@@ -146,14 +129,14 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
fun testOnFolded_stopsHingeAngleProvider() {
setFoldState(folded = true)
verify(hingeAngleProvider).stop()
assertThat(testHingeAngleProvider.isStarted).isFalse()
}
@Test
fun testOnUnfolded_startsHingeAngleProvider() {
setFoldState(folded = false)
verify(hingeAngleProvider).start()
assertThat(testHingeAngleProvider.isStarted).isTrue()
}
@Test
@@ -310,7 +293,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
@Test
fun startClosingEvent_whileNotOnLauncher_doesNotTriggerBeforeThreshold() {
setupForegroundActivityType(ACTIVITY_TYPE_STANDARD)
setupForegroundActivityType(isHomeActivity = false)
sendHingeAngleEvent(180)
sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES + 1)
@@ -318,9 +301,29 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
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(ACTIVITY_TYPE_STANDARD)
setupForegroundActivityType(isHomeActivity = false)
sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES)
sendHingeAngleEvent(START_CLOSING_ON_APPS_THRESHOLD_DEGREES - 1)
@@ -328,9 +331,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
}
private fun setupForegroundActivityType(@ActivityType type: Int) {
val taskInfo = RunningTaskInfo().apply { topActivityType = type }
whenever(activityManager.getRunningTasks(1)).thenReturn(listOf(taskInfo))
private fun setupForegroundActivityType(isHomeActivity: Boolean?) {
whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity)
}
private fun simulateTimeout(waitTime: Long = HALF_OPENED_TIMEOUT_MILLIS) {
@@ -348,16 +350,72 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
}
private fun setFoldState(folded: Boolean) {
val state = if (folded) deviceStates.folded else deviceStates.unfolded
foldStateListenerCaptor.value.onStateChanged(state)
foldProvider.notifyFolded(folded)
}
private fun fireScreenOnEvent() {
screenOnListenerCaptor.value.onScreenTurnedOn()
screenOnStatusProvider.notifyScreenTurnedOn()
}
private fun sendHingeAngleEvent(angle: Int) {
hingeAngleCaptor.value.accept(angle.toFloat())
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 {

View File

@@ -20,18 +20,18 @@ import android.view.IRotationWatcher
import android.view.IWindowManager
import android.view.Surface
import androidx.test.filters.SmallTest
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
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.SysuiTestCase
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@@ -41,16 +41,13 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
@Mock
lateinit var windowManager: IWindowManager
@Mock
lateinit var sourceProvider: UnfoldTransitionProgressProvider
private val sourceProvider = TestUnfoldTransitionProvider()
@Mock
lateinit var transitionListener: TransitionProgressListener
lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
private val sourceProviderListenerCaptor =
ArgumentCaptor.forClass(TransitionProgressListener::class.java)
private val rotationWatcherCaptor =
ArgumentCaptor.forClass(IRotationWatcher.Stub::class.java)
@@ -66,7 +63,6 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
progressProvider.init()
verify(sourceProvider).addCallback(sourceProviderListenerCaptor.capture())
verify(windowManager).watchRotation(rotationWatcherCaptor.capture(), any())
progressProvider.addCallback(transitionListener)
@@ -76,7 +72,7 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
fun testNaturalRotation0_sendTransitionStartedEvent_eventReceived() {
onRotationChanged(Surface.ROTATION_0)
source.onTransitionStarted()
sourceProvider.onTransitionStarted()
verify(transitionListener).onTransitionStarted()
}
@@ -85,7 +81,7 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
fun testNaturalRotation0_sendTransitionProgressEvent_eventReceived() {
onRotationChanged(Surface.ROTATION_0)
source.onTransitionProgress(0.5f)
sourceProvider.onTransitionProgress(0.5f)
verify(transitionListener).onTransitionProgress(0.5f)
}
@@ -94,7 +90,7 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
fun testNotNaturalRotation90_sendTransitionStartedEvent_eventNotReceived() {
onRotationChanged(Surface.ROTATION_90)
source.onTransitionStarted()
sourceProvider.onTransitionStarted()
verify(transitionListener, never()).onTransitionStarted()
}
@@ -103,7 +99,7 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
fun testNaturalRotation90_sendTransitionProgressEvent_eventNotReceived() {
onRotationChanged(Surface.ROTATION_90)
source.onTransitionProgress(0.5f)
sourceProvider.onTransitionProgress(0.5f)
verify(transitionListener, never()).onTransitionProgress(0.5f)
}
@@ -111,7 +107,7 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
@Test
fun testRotationBecameUnnaturalDuringTransition_sendsTransitionFinishedEvent() {
onRotationChanged(Surface.ROTATION_0)
source.onTransitionStarted()
sourceProvider.onTransitionStarted()
clearInvocations(transitionListener)
onRotationChanged(Surface.ROTATION_90)
@@ -122,7 +118,7 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
@Test
fun testRotationBecameNaturalDuringTransition_sendsTransitionStartedEvent() {
onRotationChanged(Surface.ROTATION_90)
source.onTransitionStarted()
sourceProvider.onTransitionStarted()
clearInvocations(transitionListener)
onRotationChanged(Surface.ROTATION_0)
@@ -133,7 +129,4 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
private fun onRotationChanged(rotation: Int) {
rotationWatcherCaptor.value.onRotationChanged(rotation)
}
private val source: TransitionProgressListener
get() = sourceProviderListenerCaptor.value
}

View File

@@ -21,6 +21,7 @@ import android.database.ContentObserver
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.TestUnfoldTransitionProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.util.mockito.any
@@ -40,16 +41,12 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
@Mock
lateinit var contentResolver: ContentResolver
@Mock
lateinit var sourceProvider: UnfoldTransitionProgressProvider
@Mock
lateinit var sinkProvider: TransitionProgressListener
lateinit var progressProvider: ScaleAwareTransitionProgressProvider
private val sourceProvider = TestUnfoldTransitionProvider()
private val sourceProviderListenerCaptor =
ArgumentCaptor.forClass(TransitionProgressListener::class.java)
lateinit var progressProvider: ScaleAwareTransitionProgressProvider
private val animatorDurationScaleListenerCaptor =
ArgumentCaptor.forClass(ContentObserver::class.java)
@@ -63,7 +60,6 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
contentResolver
)
verify(sourceProvider).addCallback(sourceProviderListenerCaptor.capture())
verify(contentResolver).registerContentObserver(any(), any(),
animatorDurationScaleListenerCaptor.capture())
@@ -74,7 +70,7 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
fun onTransitionStarted_animationsEnabled_eventReceived() {
setAnimationsEnabled(true)
source.onTransitionStarted()
sourceProvider.onTransitionStarted()
verify(sinkProvider).onTransitionStarted()
}
@@ -83,7 +79,7 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
fun onTransitionStarted_animationsNotEnabled_eventNotReceived() {
setAnimationsEnabled(false)
source.onTransitionStarted()
sourceProvider.onTransitionStarted()
verifyNoMoreInteractions(sinkProvider)
}
@@ -92,7 +88,7 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
fun onTransitionEnd_animationsEnabled_eventReceived() {
setAnimationsEnabled(true)
source.onTransitionFinished()
sourceProvider.onTransitionFinished()
verify(sinkProvider).onTransitionFinished()
}
@@ -101,7 +97,7 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
fun onTransitionEnd_animationsNotEnabled_eventNotReceived() {
setAnimationsEnabled(false)
source.onTransitionFinished()
sourceProvider.onTransitionFinished()
verifyNoMoreInteractions(sinkProvider)
}
@@ -110,7 +106,7 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
fun onTransitionProgress_animationsEnabled_eventReceived() {
setAnimationsEnabled(true)
source.onTransitionProgress(42f)
sourceProvider.onTransitionProgress(42f)
verify(sinkProvider).onTransitionProgress(42f)
}
@@ -119,7 +115,7 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
fun onTransitionProgress_animationsNotEnabled_eventNotReceived() {
setAnimationsEnabled(false)
source.onTransitionProgress(42f)
sourceProvider.onTransitionProgress(42f)
verifyNoMoreInteractions(sinkProvider)
}
@@ -133,7 +129,4 @@ class ScaleAwareUnfoldProgressProviderTest : SysuiTestCase() {
ValueAnimator.setDurationScale(durationScale)
animatorDurationScaleListenerCaptor.value.dispatchChange(/* selfChange= */false)
}
private val source: TransitionProgressListener
get() = sourceProviderListenerCaptor.value
}

View File

@@ -24,6 +24,8 @@ import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
class TestFoldStateProvider : FoldStateProvider {
private val listeners: MutableList<FoldUpdatesListener> = arrayListOf()
val hasListeners: Boolean
get() = listeners.isNotEmpty()
override fun start() {
}

View File

@@ -0,0 +1,39 @@
// Copyright (C) 2022 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 {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
}
android_library {
name: "SystemUIUnfoldLib",
srcs: [
"src/**/*.java",
"src/**/*.kt",
"src/**/*.aidl",
],
static_libs: [
"androidx.dynamicanimation_dynamicanimation",
"dagger2",
"jsr330",
],
java_version: "1.8",
min_sdk_version: "current",
plugins: ["dagger2-compiler"],
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.systemui.unfold">
</manifest>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.1.0-dev" type="baseline" client="" name="" variant="all" version="7.1.0-dev">
</issues>

View File

@@ -16,16 +16,16 @@
package com.android.systemui.unfold
import android.app.ActivityManager
import android.content.ContentResolver
import android.content.Context
import android.hardware.SensorManager
import android.hardware.devicestate.DeviceStateManager
import android.os.Handler
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.UiBackground
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.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix
import dagger.BindsInstance
import dagger.Component
@@ -51,12 +51,12 @@ internal interface UnfoldSharedComponent {
@BindsInstance context: Context,
@BindsInstance config: UnfoldTransitionConfig,
@BindsInstance screenStatusProvider: ScreenStatusProvider,
@BindsInstance deviceStateManager: DeviceStateManager,
@BindsInstance activityManager: ActivityManager,
@BindsInstance foldProvider: FoldProvider,
@BindsInstance activityTypeProvider: CurrentActivityTypeProvider,
@BindsInstance sensorManager: SensorManager,
@BindsInstance @Main handler: Handler,
@BindsInstance @Main executor: Executor,
@BindsInstance @UiBackground backgroundExecutor: Executor,
@BindsInstance @UnfoldMain handler: Handler,
@BindsInstance @UnfoldMain executor: Executor,
@BindsInstance @UnfoldBackground backgroundExecutor: Executor,
@BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
@BindsInstance contentResolver: ContentResolver = context.contentResolver
): UnfoldSharedComponent

View File

@@ -17,8 +17,8 @@
package com.android.systemui.unfold
import android.hardware.SensorManager
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldBackground
import com.android.systemui.unfold.progress.FixedTimingTransitionProgressProvider
import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider
import com.android.systemui.unfold.updates.DeviceFoldStateProvider
@@ -70,7 +70,7 @@ class UnfoldSharedModule {
fun hingeAngleProvider(
config: UnfoldTransitionConfig,
sensorManager: SensorManager,
@UiBackground executor: Executor
@UnfoldBackground executor: Executor
): HingeAngleProvider =
if (config.isHingeAngleEnabled) {
HingeSensorAngleProvider(sensorManager, executor)

View File

@@ -17,14 +17,13 @@
package com.android.systemui.unfold
import android.app.ActivityManager
import android.content.Context
import android.hardware.SensorManager
import android.hardware.devicestate.DeviceStateManager
import android.os.Handler
import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import java.util.concurrent.Executor
/**
@@ -39,8 +38,8 @@ fun createUnfoldTransitionProgressProvider(
context: Context,
config: UnfoldTransitionConfig,
screenStatusProvider: ScreenStatusProvider,
deviceStateManager: DeviceStateManager,
activityManager: ActivityManager,
foldProvider: FoldProvider,
activityTypeProvider: CurrentActivityTypeProvider,
sensorManager: SensorManager,
mainHandler: Handler,
mainExecutor: Executor,
@@ -52,8 +51,8 @@ fun createUnfoldTransitionProgressProvider(
context,
config,
screenStatusProvider,
deviceStateManager,
activityManager,
foldProvider,
activityTypeProvider,
sensorManager,
mainHandler,
mainExecutor,
@@ -64,5 +63,3 @@ fun createUnfoldTransitionProgressProvider(
?: throw IllegalStateException(
"Trying to create " +
"UnfoldTransitionProgressProvider when the transition is disabled")
fun createConfig(context: Context): UnfoldTransitionConfig = ResourceUnfoldTransitionConfig(context)

View File

@@ -15,9 +15,9 @@
*/
package com.android.systemui.unfold
import android.annotation.FloatRange
import com.android.systemui.statusbar.policy.CallbackController
import androidx.annotation.FloatRange
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.util.CallbackController
/**
* Interface that allows to receive unfold transition progress updates.

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2022 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.compat
import android.content.Context
import android.content.res.Configuration
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
import java.util.concurrent.Executor
/**
* Fold provider that notifies about fold state based on the screen size
* It could be used when no activity context is available
* TODO(b/232369816): use Jetpack WM library when non-activity contexts supported b/169740873
*/
class ScreenSizeFoldProvider(private val context: Context) : FoldProvider {
private var callbacks: MutableList<FoldCallback> = arrayListOf()
private var lastWidth: Int = 0
override fun registerCallback(callback: FoldCallback, executor: Executor) {
callbacks += callback
onConfigurationChange(context.resources.configuration)
}
override fun unregisterCallback(callback: FoldCallback) {
callbacks -= callback
}
fun onConfigurationChange(newConfig: Configuration) {
if (lastWidth == newConfig.smallestScreenWidthDp) {
return
}
if (newConfig.smallestScreenWidthDp > INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP) {
callbacks.forEach { it.onFoldUpdated(false) }
} else {
callbacks.forEach { it.onFoldUpdated(true) }
}
lastWidth = newConfig.smallestScreenWidthDp
}
}
private const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2022 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.compat
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
import java.util.concurrent.Executor
class SizeScreenStatusProvider(
private val foldProvider: FoldProvider,
private val executor: Executor
) : ScreenStatusProvider {
private val listeners: MutableList<ScreenListener> = arrayListOf()
private val callback = object : FoldProvider.FoldCallback {
override fun onFoldUpdated(isFolded: Boolean) {
if (!isFolded) {
listeners.forEach { it.onScreenTurnedOn() }
}
}
}
fun start() {
foldProvider.registerCallback(
callback,
executor
)
}
fun stop() {
foldProvider.unregisterCallback(callback)
}
override fun addCallback(listener: ScreenListener) {
listeners.add(listener)
}
override fun removeCallback(listener: ScreenListener) {
listeners.remove(listener)
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2022 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.config
import android.content.res.Resources
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ResourceUnfoldTransitionConfig @Inject constructor() : UnfoldTransitionConfig {
override val isEnabled: Boolean by lazy {
val id = Resources.getSystem()
.getIdentifier("config_unfoldTransitionEnabled", "bool", "android")
Resources.getSystem().getBoolean(id)
}
override val isHingeAngleEnabled: Boolean by lazy {
val id = Resources.getSystem()
.getIdentifier("config_unfoldTransitionHingeAngle", "bool", "android")
Resources.getSystem().getBoolean(id)
}
override val halfFoldedTimeoutMillis: Int by lazy {
val id = Resources.getSystem()
.getIdentifier("config_unfoldTransitionHalfFoldedTimeout", "integer", "android")
Resources.getSystem().getInteger(id)
}
}

View File

@@ -18,4 +18,5 @@ package com.android.systemui.unfold.config
interface UnfoldTransitionConfig {
val isEnabled: Boolean
val isHingeAngleEnabled: Boolean
val halfFoldedTimeoutMillis: Int
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2022 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.dagger
import javax.inject.Qualifier
/**
* Alternative to [UiBackground] qualifier annotation in unfold module.
* It is needed as we can't depend on SystemUI code in this module.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UnfoldBackground

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2022 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.dagger
import javax.inject.Qualifier
/**
* Alternative to [Main] qualifier annotation in unfold module.
* It is needed as we can't depend on SystemUI code in this module.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UnfoldMain

View File

@@ -16,7 +16,6 @@
package com.android.systemui.unfold.progress
import android.util.Log
import android.util.MathUtils.saturate
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
@@ -70,6 +69,9 @@ class PhysicsBasedUnfoldTransitionProgressProvider(
springAnimation.animateToFinalPosition(progress)
}
private fun saturate(amount: Float, low: Float = 0f, high: Float = 1f): Float =
if (amount < low) low else if (amount > high) high else amount
override fun onFoldUpdate(@FoldUpdate update: Int) {
when (update) {
FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> {

View File

@@ -15,46 +15,46 @@
*/
package com.android.systemui.unfold.updates
import android.annotation.FloatRange
import android.app.ActivityManager
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.content.Context
import android.hardware.devicestate.DeviceStateManager
import android.os.Handler
import android.util.Log
import androidx.annotation.FloatRange
import androidx.annotation.VisibleForTesting
import androidx.core.util.Consumer
import com.android.systemui.dagger.qualifiers.Main
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.hinge.FULLY_CLOSED_DEGREES
import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import java.util.concurrent.Executor
import javax.inject.Inject
class DeviceFoldStateProvider
@Inject
constructor(
context: Context,
config: UnfoldTransitionConfig,
private val hingeAngleProvider: HingeAngleProvider,
private val screenStatusProvider: ScreenStatusProvider,
private val deviceStateManager: DeviceStateManager,
private val activityManager: ActivityManager,
@Main private val mainExecutor: Executor,
@Main private val handler: Handler
private val foldProvider: FoldProvider,
private val activityTypeProvider: CurrentActivityTypeProvider,
@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(context)
private val foldStateListener = FoldStateListener()
private val timeoutRunnable = TimeoutRunnable()
/**
@@ -62,22 +62,20 @@ constructor(
* [FOLD_UPDATE_START_CLOSING] or [FOLD_UPDATE_START_OPENING] event, if an end state is not
* reached.
*/
private val halfOpenedTimeoutMillis: Int =
context.resources.getInteger(
com.android.internal.R.integer.config_unfoldTransitionHalfFoldedTimeout)
private val halfOpenedTimeoutMillis: Int = config.halfFoldedTimeoutMillis
private var isFolded = false
private var isUnfoldHandled = true
override fun start() {
deviceStateManager.registerCallback(mainExecutor, foldStateListener)
foldProvider.registerCallback(foldStateListener, mainExecutor)
screenStatusProvider.addCallback(screenListener)
hingeAngleProvider.addCallback(hingeAngleListener)
}
override fun stop() {
screenStatusProvider.removeCallback(screenListener)
deviceStateManager.unregisterCallback(foldStateListener)
foldProvider.unregisterCallback(foldStateListener)
hingeAngleProvider.removeCallback(hingeAngleListener)
hingeAngleProvider.stop()
}
@@ -92,13 +90,13 @@ constructor(
override val isFinishedOpening: Boolean
get() = !isFolded &&
(lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN ||
lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
(lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_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) {
@@ -136,39 +134,36 @@ constructor(
* apps that support table-top/HALF_FOLDED mode. Only for launcher, there is no threshold.
*/
private fun getClosingThreshold(): Int? {
val activityType =
activityManager.getRunningTasks(/* maxNum= */ 1)?.getOrNull(0)?.topActivityType
?: return null
val isHomeActivity = activityTypeProvider.isHomeActivity ?: return null
if (DEBUG) {
Log.d(TAG, "activityType=" + activityType)
Log.d(TAG, "isHomeActivity=$isHomeActivity")
}
return if (activityType == ACTIVITY_TYPE_HOME) {
return if (isHomeActivity) {
null
} else {
START_CLOSING_ON_APPS_THRESHOLD_DEGREES
}
}
private inner class FoldStateListener(context: Context) :
DeviceStateManager.FoldStateListener(
context,
{ folded: Boolean ->
isFolded = folded
lastHingeAngle = FULLY_CLOSED_DEGREES
private inner class FoldStateListener : FoldProvider.FoldCallback {
override fun onFoldUpdated(isFolded: Boolean) {
this@DeviceFoldStateProvider.isFolded = isFolded
lastHingeAngle = FULLY_CLOSED_DEGREES
if (folded) {
hingeAngleProvider.stop()
notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
cancelTimeout()
isUnfoldHandled = false
} else {
notifyFoldUpdate(FOLD_UPDATE_START_OPENING)
rescheduleAbortAnimationTimeout()
hingeAngleProvider.start()
}
})
if (isFolded) {
hingeAngleProvider.stop()
notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
cancelTimeout()
isUnfoldHandled = false
} else {
notifyFoldUpdate(FOLD_UPDATE_START_OPENING)
rescheduleAbortAnimationTimeout()
hingeAngleProvider.start()
}
}
}
private fun notifyFoldUpdate(@FoldUpdate update: Int) {
if (DEBUG) {
@@ -234,7 +229,9 @@ 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

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 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 java.util.concurrent.Executor
interface FoldProvider {
fun registerCallback(callback: FoldCallback, executor: Executor)
fun unregisterCallback(callback: FoldCallback)
interface FoldCallback {
fun onFoldUpdated(isFolded: Boolean)
}
}

View File

@@ -15,10 +15,10 @@
*/
package com.android.systemui.unfold.updates
import android.annotation.FloatRange
import android.annotation.IntDef
import com.android.systemui.statusbar.policy.CallbackController
import androidx.annotation.FloatRange
import androidx.annotation.IntDef
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
import com.android.systemui.unfold.util.CallbackController
/**
* Allows to subscribe to main events related to fold/unfold process such as hinge angle update,
@@ -36,7 +36,6 @@ interface FoldStateProvider : CallbackController<FoldUpdatesListener> {
}
@IntDef(
prefix = ["FOLD_UPDATE_"],
value =
[
FOLD_UPDATE_START_OPENING,

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2022 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.hinge
import androidx.core.util.Consumer
internal object EmptyHingeAngleProvider : HingeAngleProvider {
override fun start() {}
override fun stop() {}
override fun removeCallback(listener: Consumer<Float>) {}
override fun addCallback(listener: Consumer<Float>) {}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 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.hinge
import androidx.core.util.Consumer
import com.android.systemui.unfold.util.CallbackController
/**
* Emits device hinge angle values (angle between two integral parts of the device).
*
* The hinge angle could be from 0 to 360 degrees inclusive. For foldable devices usually 0
* corresponds to fully closed (folded) state and 180 degrees corresponds to fully open (flat)
* state.
*/
interface HingeAngleProvider : CallbackController<Consumer<Float>> {
fun start()
fun stop()
}
const val FULLY_OPEN_DEGREES = 180f
const val FULLY_CLOSED_DEGREES = 0f

View File

@@ -1,3 +1,17 @@
/*
* Copyright (C) 2022 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.hinge
import android.hardware.Sensor

View File

@@ -15,8 +15,8 @@
*/
package com.android.systemui.unfold.updates.screen
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
import com.android.systemui.unfold.util.CallbackController
interface ScreenStatusProvider : CallbackController<ScreenListener> {

View File

@@ -1,3 +1,17 @@
/*
* Copyright (C) 2022 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.util
import android.os.Trace

View File

@@ -0,0 +1,20 @@
/*
* Copyright (C) 2022 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.util
interface CallbackController<T> {
fun addCallback(listener: T)
fun removeCallback(listener: T)
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2022 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.util
interface CurrentActivityTypeProvider {
val isHomeActivity: Boolean?
}
class EmptyCurrentActivityTypeProvider(override val isHomeActivity: Boolean? = null) :
CurrentActivityTypeProvider

View File

@@ -1,3 +1,17 @@
/*
* Copyright (C) 2022 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.util
import android.animation.ValueAnimator