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
diff --git a/packages/SystemUI/unfold/Android.bp b/packages/SystemUI/unfold/Android.bp
new file mode 100644
index 0000000..108295b
--- /dev/null
+++ b/packages/SystemUI/unfold/Android.bp
@@ -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"],
+}
diff --git a/packages/SystemUI/unfold/AndroidManifest.xml b/packages/SystemUI/unfold/AndroidManifest.xml
new file mode 100644
index 0000000..ee8afe1
--- /dev/null
+++ b/packages/SystemUI/unfold/AndroidManifest.xml
@@ -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>
diff --git a/packages/SystemUI/unfold/lint-baseline.xml b/packages/SystemUI/unfold/lint-baseline.xml
new file mode 100644
index 0000000..449ed2e
--- /dev/null
+++ b/packages/SystemUI/unfold/lint-baseline.xml
@@ -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>
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
new file mode 100644
index 0000000..a5ec0a4
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
@@ -0,0 +1,66 @@
+/*
+ * 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
+
+import android.content.ContentResolver
+import android.content.Context
+import android.hardware.SensorManager
+import android.os.Handler
+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
+import java.util.Optional
+import java.util.concurrent.Executor
+import javax.inject.Singleton
+
+/**
+ * Provides [UnfoldTransitionProgressProvider]. The [Optional] is empty when the transition
+ * animation is disabled.
+ *
+ * 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.
+ */
+@Singleton
+@Component(modules = [UnfoldSharedModule::class])
+internal interface UnfoldSharedComponent {
+
+ @Component.Factory
+ interface Factory {
+ fun create(
+ @BindsInstance context: Context,
+ @BindsInstance config: UnfoldTransitionConfig,
+ @BindsInstance screenStatusProvider: ScreenStatusProvider,
+ @BindsInstance foldProvider: FoldProvider,
+ @BindsInstance activityTypeProvider: CurrentActivityTypeProvider,
+ @BindsInstance sensorManager: SensorManager,
+ @BindsInstance @UnfoldMain handler: Handler,
+ @BindsInstance @UnfoldMain executor: Executor,
+ @BindsInstance @UnfoldBackground backgroundExecutor: Executor,
+ @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
+ @BindsInstance contentResolver: ContentResolver = context.contentResolver
+ ): UnfoldSharedComponent
+ }
+
+ val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider>
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt
new file mode 100644
index 0000000..8f4ee4d
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt
@@ -0,0 +1,80 @@
+/*
+ * 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
+
+import android.hardware.SensorManager
+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
+import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.unfold.updates.hinge.EmptyHingeAngleProvider
+import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
+import com.android.systemui.unfold.updates.hinge.HingeSensorAngleProvider
+import com.android.systemui.unfold.util.ATraceLoggerTransitionProgressListener
+import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider
+import dagger.Module
+import dagger.Provides
+import java.util.Optional
+import java.util.concurrent.Executor
+import javax.inject.Singleton
+
+@Module
+class UnfoldSharedModule {
+ @Provides
+ @Singleton
+ fun unfoldTransitionProgressProvider(
+ config: UnfoldTransitionConfig,
+ scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory,
+ tracingListener: ATraceLoggerTransitionProgressListener,
+ foldStateProvider: FoldStateProvider
+ ): Optional<UnfoldTransitionProgressProvider> =
+ if (!config.isEnabled) {
+ Optional.empty()
+ } else {
+ val baseProgressProvider =
+ if (config.isHingeAngleEnabled) {
+ PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider)
+ } else {
+ FixedTimingTransitionProgressProvider(foldStateProvider)
+ }
+ Optional.of(
+ scaleAwareProviderFactory.wrap(baseProgressProvider).apply {
+ // Always present callback that logs animation beginning and end.
+ addCallback(tracingListener)
+ })
+ }
+
+ @Provides
+ @Singleton
+ fun provideFoldStateProvider(
+ deviceFoldStateProvider: DeviceFoldStateProvider
+ ): FoldStateProvider = deviceFoldStateProvider
+
+ @Provides
+ fun hingeAngleProvider(
+ config: UnfoldTransitionConfig,
+ sensorManager: SensorManager,
+ @UnfoldBackground executor: Executor
+ ): HingeAngleProvider =
+ if (config.isHingeAngleEnabled) {
+ HingeSensorAngleProvider(sensorManager, executor)
+ } else {
+ EmptyHingeAngleProvider
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
new file mode 100644
index 0000000..402dd84
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+@file:JvmName("UnfoldTransitionFactory")
+
+package com.android.systemui.unfold
+
+import android.content.Context
+import android.hardware.SensorManager
+import android.os.Handler
+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
+
+/**
+ * Factory for [UnfoldTransitionProgressProvider].
+ *
+ * This 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(
+ context: Context,
+ config: UnfoldTransitionConfig,
+ screenStatusProvider: ScreenStatusProvider,
+ foldProvider: FoldProvider,
+ activityTypeProvider: CurrentActivityTypeProvider,
+ sensorManager: SensorManager,
+ mainHandler: Handler,
+ mainExecutor: Executor,
+ backgroundExecutor: Executor,
+ tracingTagPrefix: String
+): UnfoldTransitionProgressProvider =
+ DaggerUnfoldSharedComponent.factory()
+ .create(
+ context,
+ config,
+ screenStatusProvider,
+ foldProvider,
+ activityTypeProvider,
+ sensorManager,
+ mainHandler,
+ mainExecutor,
+ backgroundExecutor,
+ tracingTagPrefix)
+ .unfoldTransitionProvider
+ .orElse(null)
+ ?: throw IllegalStateException(
+ "Trying to create " +
+ "UnfoldTransitionProgressProvider when the transition is disabled")
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
new file mode 100644
index 0000000..d54481c
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
@@ -0,0 +1,40 @@
+/*
+ * 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
+
+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.
+ *
+ * It can be used to update view properties based on the current animation progress.
+ *
+ * onTransitionProgress callback could be called on each frame.
+ *
+ * Use [createUnfoldTransitionProgressProvider] to create instances of this interface
+ */
+interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgressListener> {
+
+ fun destroy()
+
+ interface TransitionProgressListener {
+ fun onTransitionStarted() {}
+ fun onTransitionFinished() {}
+ fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/compat/ScreenSizeFoldProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/compat/ScreenSizeFoldProvider.kt
new file mode 100644
index 0000000..2044f05
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/compat/ScreenSizeFoldProvider.kt
@@ -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
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/compat/SizeScreenStatusProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/compat/SizeScreenStatusProvider.kt
new file mode 100644
index 0000000..c405f31
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/compat/SizeScreenStatusProvider.kt
@@ -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)
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt
new file mode 100644
index 0000000..c513729
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt
@@ -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)
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt
new file mode 100644
index 0000000..765e862
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/config/UnfoldTransitionConfig.kt
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+interface UnfoldTransitionConfig {
+ val isEnabled: Boolean
+ val isHingeAngleEnabled: Boolean
+ val halfFoldedTimeoutMillis: Int
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt
new file mode 100644
index 0000000..6074795
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt
@@ -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
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldMain.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldMain.kt
new file mode 100644
index 0000000..5553690f
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldMain.kt
@@ -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
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
new file mode 100644
index 0000000..4c85b05
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.progress
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.util.FloatProperty
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
+import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
+import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
+
+/** Emits animation progress with fixed timing after unfolding */
+internal class FixedTimingTransitionProgressProvider(
+ private val foldStateProvider: FoldStateProvider
+) : UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener {
+
+ private val animatorListener = AnimatorListener()
+ private val animator =
+ ObjectAnimator.ofFloat(this, AnimationProgressProperty, 0f, 1f).apply {
+ duration = TRANSITION_TIME_MILLIS
+ addListener(animatorListener)
+ }
+
+ private var transitionProgress: Float = 0.0f
+ set(value) {
+ listeners.forEach { it.onTransitionProgress(value) }
+ field = value
+ }
+
+ private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
+
+ init {
+ foldStateProvider.addCallback(this)
+ foldStateProvider.start()
+ }
+
+ override fun destroy() {
+ animator.cancel()
+ foldStateProvider.removeCallback(this)
+ foldStateProvider.stop()
+ }
+
+ override fun onFoldUpdate(@FoldUpdate update: Int) {
+ when (update) {
+ FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> animator.start()
+ FOLD_UPDATE_FINISH_CLOSED -> animator.cancel()
+ }
+ }
+
+ override fun addCallback(listener: TransitionProgressListener) {
+ listeners.add(listener)
+ }
+
+ override fun removeCallback(listener: TransitionProgressListener) {
+ listeners.remove(listener)
+ }
+
+ override fun onHingeAngleUpdate(angle: Float) {}
+
+ private object AnimationProgressProperty :
+ FloatProperty<FixedTimingTransitionProgressProvider>("animation_progress") {
+
+ override fun setValue(provider: FixedTimingTransitionProgressProvider, value: Float) {
+ provider.transitionProgress = value
+ }
+
+ override fun get(provider: FixedTimingTransitionProgressProvider): Float =
+ provider.transitionProgress
+ }
+
+ private inner class AnimatorListener : Animator.AnimatorListener {
+
+ override fun onAnimationStart(animator: Animator) {
+ listeners.forEach { it.onTransitionStarted() }
+ }
+
+ override fun onAnimationEnd(animator: Animator) {
+ listeners.forEach { it.onTransitionFinished() }
+ }
+
+ override fun onAnimationRepeat(animator: Animator) {}
+
+ override fun onAnimationCancel(animator: Animator) {}
+ }
+
+ private companion object {
+ private const val TRANSITION_TIME_MILLIS = 400L
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
new file mode 100644
index 0000000..2ab28c6
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.progress
+
+import android.util.Log
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
+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_UNFOLDED_SCREEN_AVAILABLE
+import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
+import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
+
+/** Maps fold updates to unfold transition progress using DynamicAnimation. */
+class PhysicsBasedUnfoldTransitionProgressProvider(
+ private val foldStateProvider: FoldStateProvider
+) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {
+
+ private val springAnimation =
+ SpringAnimation(this, AnimationProgressProperty).apply {
+ addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider)
+ }
+
+ private var isTransitionRunning = false
+ private var isAnimatedCancelRunning = false
+
+ private var transitionProgress: Float = 0.0f
+ set(value) {
+ if (isTransitionRunning) {
+ listeners.forEach { it.onTransitionProgress(value) }
+ }
+ field = value
+ }
+
+ private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
+
+ init {
+ foldStateProvider.addCallback(this)
+ foldStateProvider.start()
+ }
+
+ override fun destroy() {
+ foldStateProvider.stop()
+ }
+
+ override fun onHingeAngleUpdate(angle: Float) {
+ if (!isTransitionRunning || isAnimatedCancelRunning) return
+ val progress = saturate(angle / FINAL_HINGE_ANGLE_POSITION)
+ 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 -> {
+ startTransition(startValue = 0f)
+
+ // Stop the animation if the device has already opened by the time when
+ // the display is available as we won't receive the full open event anymore
+ if (foldStateProvider.isFinishedOpening) {
+ cancelTransition(endValue = 1f, animate = true)
+ }
+ }
+ FOLD_UPDATE_FINISH_FULL_OPEN, FOLD_UPDATE_FINISH_HALF_OPEN -> {
+ // Do not cancel if we haven't started the transition yet.
+ // This could happen when we fully unfolded the device before the screen
+ // became available. In this case we start and immediately cancel the animation
+ // in FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE event handler, so we don't need to
+ // cancel it here.
+ if (isTransitionRunning) {
+ cancelTransition(endValue = 1f, animate = true)
+ }
+ }
+ FOLD_UPDATE_FINISH_CLOSED -> {
+ cancelTransition(endValue = 0f, animate = false)
+ }
+ FOLD_UPDATE_START_CLOSING -> {
+ // The transition might be already running as the device might start closing several
+ // times before reaching an end state.
+ if (isTransitionRunning) {
+ // If we are cancelling the animation, reset that so we can resume it normally.
+ // The animation could be 'cancelled' when the user stops folding/unfolding
+ // for some period of time or fully unfolds the device. In this case,
+ // it is forced to run to the end ignoring all further hinge angle events.
+ // By resetting this flag we allow reacting to hinge angle events again, so
+ // the transition continues running.
+ if (isAnimatedCancelRunning) {
+ isAnimatedCancelRunning = false
+ }
+ } else {
+ startTransition(startValue = 1f)
+ }
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "onFoldUpdate = $update")
+ }
+ }
+
+ private fun cancelTransition(endValue: Float, animate: Boolean) {
+ if (isTransitionRunning && animate) {
+ isAnimatedCancelRunning = true
+ springAnimation.animateToFinalPosition(endValue)
+ } else {
+ transitionProgress = endValue
+ isAnimatedCancelRunning = false
+ isTransitionRunning = false
+ springAnimation.cancel()
+
+ listeners.forEach { it.onTransitionFinished() }
+
+ if (DEBUG) {
+ Log.d(TAG, "onTransitionFinished")
+ }
+ }
+ }
+
+ override fun onAnimationEnd(
+ animation: DynamicAnimation<out DynamicAnimation<*>>,
+ canceled: Boolean,
+ value: Float,
+ velocity: Float
+ ) {
+ if (isAnimatedCancelRunning) {
+ cancelTransition(value, animate = false)
+ }
+ }
+
+ private fun onStartTransition() {
+ listeners.forEach { it.onTransitionStarted() }
+ isTransitionRunning = true
+
+ if (DEBUG) {
+ Log.d(TAG, "onTransitionStarted")
+ }
+ }
+
+ private fun startTransition(startValue: Float) {
+ if (!isTransitionRunning) onStartTransition()
+
+ springAnimation.apply {
+ spring =
+ SpringForce().apply {
+ finalPosition = startValue
+ dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
+ stiffness = SPRING_STIFFNESS
+ }
+ minimumVisibleChange = MINIMAL_VISIBLE_CHANGE
+ setStartValue(startValue)
+ setMinValue(0f)
+ setMaxValue(1f)
+ }
+
+ springAnimation.start()
+ }
+
+ override fun addCallback(listener: TransitionProgressListener) {
+ listeners.add(listener)
+ }
+
+ override fun removeCallback(listener: TransitionProgressListener) {
+ listeners.remove(listener)
+ }
+
+ private object AnimationProgressProperty :
+ FloatPropertyCompat<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") {
+
+ override fun setValue(
+ provider: PhysicsBasedUnfoldTransitionProgressProvider,
+ value: Float
+ ) {
+ provider.transitionProgress = value
+ }
+
+ override fun getValue(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float =
+ provider.transitionProgress
+ }
+}
+
+private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider"
+private const val DEBUG = true
+
+private const val SPRING_STIFFNESS = 200.0f
+private const val MINIMAL_VISIBLE_CHANGE = 0.001f
+private const val FINAL_HINGE_ANGLE_POSITION = 165f
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
new file mode 100644
index 0000000..e8038fd
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.updates
+
+import android.os.Handler
+import android.util.Log
+import androidx.annotation.FloatRange
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer
+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(
+ config: UnfoldTransitionConfig,
+ private val hingeAngleProvider: HingeAngleProvider,
+ private val screenStatusProvider: ScreenStatusProvider,
+ 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
+
+ @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()
+
+ /**
+ * Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
+ * [FOLD_UPDATE_START_CLOSING] or [FOLD_UPDATE_START_OPENING] event, if an end state is not
+ * reached.
+ */
+ private val halfOpenedTimeoutMillis: Int = config.halfFoldedTimeoutMillis
+
+ private var isFolded = false
+ private var isUnfoldHandled = true
+
+ override fun start() {
+ foldProvider.registerCallback(foldStateListener, mainExecutor)
+ screenStatusProvider.addCallback(screenListener)
+ hingeAngleProvider.addCallback(hingeAngleListener)
+ }
+
+ override fun stop() {
+ screenStatusProvider.removeCallback(screenListener)
+ foldProvider.unregisterCallback(foldStateListener)
+ hingeAngleProvider.removeCallback(hingeAngleListener)
+ hingeAngleProvider.stop()
+ }
+
+ override fun addCallback(listener: FoldUpdatesListener) {
+ outputListeners.add(listener)
+ }
+
+ override fun removeCallback(listener: FoldUpdatesListener) {
+ outputListeners.remove(listener)
+ }
+
+ override val isFinishedOpening: Boolean
+ get() = !isFolded &&
+ (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
+
+ private fun onHingeAngle(angle: Float) {
+ if (DEBUG) {
+ Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle")
+ }
+
+ val isClosing = angle < lastHingeAngle
+ val closingThreshold = getClosingThreshold()
+ val closingThresholdMet = closingThreshold == null || angle < closingThreshold
+ val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
+ val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING
+
+ if (isClosing && closingThresholdMet && !closingEventDispatched && !isFullyOpened) {
+ notifyFoldUpdate(FOLD_UPDATE_START_CLOSING)
+ }
+
+ if (isTransitionInProgress) {
+ if (isFullyOpened) {
+ notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
+ cancelTimeout()
+ } else {
+ // The timeout will trigger some constant time after the last angle update.
+ rescheduleAbortAnimationTimeout()
+ }
+ }
+
+ lastHingeAngle = angle
+ outputListeners.forEach { it.onHingeAngleUpdate(angle) }
+ }
+
+ /**
+ * Fold animation should be started only after the threshold returned here.
+ *
+ * This has been introduced because the fold animation might be distracting/unwanted on top of
+ * apps that support table-top/HALF_FOLDED mode. Only for launcher, there is no threshold.
+ */
+ private fun getClosingThreshold(): Int? {
+ val isHomeActivity = activityTypeProvider.isHomeActivity ?: return null
+
+ if (DEBUG) {
+ Log.d(TAG, "isHomeActivity=$isHomeActivity")
+ }
+
+ return if (isHomeActivity) {
+ null
+ } else {
+ START_CLOSING_ON_APPS_THRESHOLD_DEGREES
+ }
+ }
+
+ private inner class FoldStateListener : FoldProvider.FoldCallback {
+ override fun onFoldUpdated(isFolded: Boolean) {
+ this@DeviceFoldStateProvider.isFolded = isFolded
+ lastHingeAngle = FULLY_CLOSED_DEGREES
+
+ 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) {
+ Log.d(TAG, stateToString(update))
+ }
+ outputListeners.forEach { it.onFoldUpdate(update) }
+ lastFoldUpdate = update
+ }
+
+ private fun rescheduleAbortAnimationTimeout() {
+ if (isTransitionInProgress) {
+ cancelTimeout()
+ }
+ handler.postDelayed(timeoutRunnable, halfOpenedTimeoutMillis.toLong())
+ }
+
+ private fun cancelTimeout() {
+ handler.removeCallbacks(timeoutRunnable)
+ }
+
+ private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
+
+ override fun onScreenTurnedOn() {
+ // Trigger this event only if we are unfolded and this is the first screen
+ // turned on event since unfold started. This prevents running the animation when
+ // turning on the internal display using the power button.
+ // Initially isUnfoldHandled is true so it will be reset to false *only* when we
+ // receive 'folded' event. If SystemUI started when device is already folded it will
+ // still receive 'folded' event on startup.
+ if (!isFolded && !isUnfoldHandled) {
+ outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) }
+ isUnfoldHandled = true
+ }
+ }
+ }
+
+ private inner class HingeAngleListener : Consumer<Float> {
+ override fun accept(angle: Float) {
+ 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) {
+ FOLD_UPDATE_START_OPENING -> "START_OPENING"
+ FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
+ FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE"
+ FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN"
+ FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN"
+ 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
+
+/** Fold animation on top of apps only when the angle exceeds this threshold. */
+@VisibleForTesting
+const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldProvider.kt
new file mode 100644
index 0000000..6e87bee
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldProvider.kt
@@ -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)
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateProvider.kt
new file mode 100644
index 0000000..c7a8bf3
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateProvider.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.updates
+
+import 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,
+ * start folding/unfolding, screen availability
+ */
+interface FoldStateProvider : CallbackController<FoldUpdatesListener> {
+ fun start()
+ fun stop()
+
+ val isFinishedOpening: Boolean
+
+ interface FoldUpdatesListener {
+ fun onHingeAngleUpdate(@FloatRange(from = 0.0, to = 180.0) angle: Float)
+ fun onFoldUpdate(@FoldUpdate update: Int)
+ }
+
+ @IntDef(
+ value =
+ [
+ FOLD_UPDATE_START_OPENING,
+ FOLD_UPDATE_START_CLOSING,
+ FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE,
+ FOLD_UPDATE_FINISH_HALF_OPEN,
+ FOLD_UPDATE_FINISH_FULL_OPEN,
+ FOLD_UPDATE_FINISH_CLOSED])
+ @Retention(AnnotationRetention.SOURCE)
+ annotation class FoldUpdate
+}
+
+const val FOLD_UPDATE_START_OPENING = 0
+const val FOLD_UPDATE_START_CLOSING = 1
+const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 2
+const val FOLD_UPDATE_FINISH_HALF_OPEN = 3
+const val FOLD_UPDATE_FINISH_FULL_OPEN = 4
+const val FOLD_UPDATE_FINISH_CLOSED = 5
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/EmptyHingeAngleProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/EmptyHingeAngleProvider.kt
new file mode 100644
index 0000000..e985506
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/EmptyHingeAngleProvider.kt
@@ -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>) {}
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeAngleProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeAngleProvider.kt
new file mode 100644
index 0000000..e464c3f
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeAngleProvider.kt
@@ -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
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
new file mode 100644
index 0000000..3fc5d61
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
@@ -0,0 +1,65 @@
+/*
+ * 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
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import android.os.Trace
+import androidx.core.util.Consumer
+import java.util.concurrent.Executor
+
+internal class HingeSensorAngleProvider(
+ private val sensorManager: SensorManager,
+ private val executor: Executor
+) :
+ HingeAngleProvider {
+
+ private val sensorListener = HingeAngleSensorListener()
+ private val listeners: MutableList<Consumer<Float>> = arrayListOf()
+
+ override fun start() = executor.execute {
+ Trace.beginSection("HingeSensorAngleProvider#start")
+ val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
+ sensorManager.registerListener(
+ sensorListener,
+ sensor,
+ SensorManager.SENSOR_DELAY_FASTEST
+ )
+ Trace.endSection()
+ }
+
+ override fun stop() = executor.execute {
+ sensorManager.unregisterListener(sensorListener)
+ }
+
+ override fun removeCallback(listener: Consumer<Float>) {
+ listeners.remove(listener)
+ }
+
+ override fun addCallback(listener: Consumer<Float>) {
+ listeners.add(listener)
+ }
+
+ private inner class HingeAngleSensorListener : SensorEventListener {
+
+ override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
+
+ override fun onSensorChanged(event: SensorEvent) {
+ listeners.forEach { it.accept(event.values[0]) }
+ }
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt
new file mode 100644
index 0000000..d95e050
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.unfold.updates.screen
+
+import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
+import com.android.systemui.unfold.util.CallbackController
+
+interface ScreenStatusProvider : CallbackController<ScreenListener> {
+
+ interface ScreenListener {
+ /**
+ * Called when the screen is on and ready (windows are drawn and screen blocker is removed)
+ */
+ fun onScreenTurnedOn()
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ATraceLoggerTransitionProgressListener.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ATraceLoggerTransitionProgressListener.kt
new file mode 100644
index 0000000..d8bc018
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ATraceLoggerTransitionProgressListener.kt
@@ -0,0 +1,50 @@
+/*
+ * 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
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import javax.inject.Inject
+import javax.inject.Qualifier
+
+/**
+ * Listener that logs start and end of the fold-unfold transition.
+ *
+ * [tracePrefix] arg helps in differentiating those. Currently, this is expected to be logged twice
+ * for each fold/unfold: in (1) systemui and (2) launcher process.
+ */
+class ATraceLoggerTransitionProgressListener
+@Inject
+internal constructor(@UnfoldTransitionATracePrefix tracePrefix: String) :
+ TransitionProgressListener {
+
+ private val traceName = "$tracePrefix#$UNFOLD_TRANSITION_TRACE_NAME"
+
+ override fun onTransitionStarted() {
+ Trace.beginAsyncSection(traceName, /* cookie= */ 0)
+ }
+
+ override fun onTransitionFinished() {
+ Trace.endAsyncSection(traceName, /* cookie= */ 0)
+ }
+
+ override fun onTransitionProgress(progress: Float) {
+ Trace.setCounter(traceName, (progress * 100).toLong())
+ }
+}
+
+private const val UNFOLD_TRANSITION_TRACE_NAME = "FoldUnfoldTransitionInProgress"
+
+@Qualifier annotation class UnfoldTransitionATracePrefix
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CallbackController.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CallbackController.kt
new file mode 100644
index 0000000..46ad534
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CallbackController.kt
@@ -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)
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt
new file mode 100644
index 0000000..d0e6cdc
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt
@@ -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
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt
new file mode 100644
index 0000000..5c92b34
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScaleAwareTransitionProgressProvider.kt
@@ -0,0 +1,77 @@
+/*
+ * 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
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.provider.Settings
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Wraps [UnfoldTransitionProgressProvider] to disable transitions when animations are disabled. */
+class ScaleAwareTransitionProgressProvider
+@AssistedInject
+constructor(
+ @Assisted progressProviderToWrap: UnfoldTransitionProgressProvider,
+ private val contentResolver: ContentResolver
+) : UnfoldTransitionProgressProvider {
+
+ private val scopedUnfoldTransitionProgressProvider =
+ ScopedUnfoldTransitionProgressProvider(progressProviderToWrap)
+
+ private val animatorDurationScaleObserver =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ onAnimatorScaleChanged()
+ }
+ }
+
+ init {
+ contentResolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+ /* notifyForDescendants= */ false,
+ animatorDurationScaleObserver)
+ onAnimatorScaleChanged()
+ }
+
+ private fun onAnimatorScaleChanged() {
+ val animationsEnabled = ValueAnimator.areAnimatorsEnabled()
+ scopedUnfoldTransitionProgressProvider.setReadyToHandleTransition(animationsEnabled)
+ }
+
+ override fun addCallback(listener: TransitionProgressListener) {
+ scopedUnfoldTransitionProgressProvider.addCallback(listener)
+ }
+
+ override fun removeCallback(listener: TransitionProgressListener) {
+ scopedUnfoldTransitionProgressProvider.removeCallback(listener)
+ }
+
+ override fun destroy() {
+ contentResolver.unregisterContentObserver(animatorDurationScaleObserver)
+ scopedUnfoldTransitionProgressProvider.destroy()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun wrap(
+ progressProvider: UnfoldTransitionProgressProvider
+ ): ScaleAwareTransitionProgressProvider
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt
new file mode 100644
index 0000000..8491f83
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.util
+
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+
+/**
+ * Manages progress listeners that can have smaller lifespan than the unfold animation.
+ *
+ * Allows to limit getting transition updates to only when
+ * [ScopedUnfoldTransitionProgressProvider.setReadyToHandleTransition] is called with
+ * readyToHandleTransition = true
+ *
+ * If the transition has already started by the moment when the clients are ready to play the
+ * transition then it will report transition started callback and current animation progress.
+ */
+open class ScopedUnfoldTransitionProgressProvider
+@JvmOverloads
+constructor(source: UnfoldTransitionProgressProvider? = null) :
+ UnfoldTransitionProgressProvider, TransitionProgressListener {
+
+ private var source: UnfoldTransitionProgressProvider? = null
+
+ private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
+
+ private var isReadyToHandleTransition = false
+ private var isTransitionRunning = false
+ private var lastTransitionProgress = PROGRESS_UNSET
+
+ init {
+ setSourceProvider(source)
+ }
+ /**
+ * Sets the source for the unfold transition progress updates. Replaces current provider if it
+ * is already set
+ * @param provider transition provider that emits transition progress updates
+ */
+ fun setSourceProvider(provider: UnfoldTransitionProgressProvider?) {
+ source?.removeCallback(this)
+
+ if (provider != null) {
+ source = provider
+ provider.addCallback(this)
+ } else {
+ source = null
+ }
+ }
+
+ /**
+ * Allows to notify this provide whether the listeners can play the transition or not.
+ *
+ * Call this method with readyToHandleTransition = true when all listeners are ready to consume
+ * the transition progress events.
+ *
+ * Call it with readyToHandleTransition = false when listeners can't process the events.
+ */
+ fun setReadyToHandleTransition(isReadyToHandleTransition: Boolean) {
+ if (isTransitionRunning) {
+ if (isReadyToHandleTransition) {
+ listeners.forEach { it.onTransitionStarted() }
+ if (lastTransitionProgress != PROGRESS_UNSET) {
+ listeners.forEach { it.onTransitionProgress(lastTransitionProgress) }
+ }
+ } else {
+ isTransitionRunning = false
+ listeners.forEach { it.onTransitionFinished() }
+ }
+ }
+ this.isReadyToHandleTransition = isReadyToHandleTransition
+ }
+
+ override fun addCallback(listener: TransitionProgressListener) {
+ listeners += listener
+ }
+
+ override fun removeCallback(listener: TransitionProgressListener) {
+ listeners -= listener
+ }
+
+ override fun destroy() {
+ source?.removeCallback(this)
+ source?.destroy()
+ }
+
+ override fun onTransitionStarted() {
+ isTransitionRunning = true
+ if (isReadyToHandleTransition) {
+ listeners.forEach { it.onTransitionStarted() }
+ }
+ }
+
+ override fun onTransitionProgress(progress: Float) {
+ if (isReadyToHandleTransition) {
+ listeners.forEach { it.onTransitionProgress(progress) }
+ }
+ lastTransitionProgress = progress
+ }
+
+ override fun onTransitionFinished() {
+ if (isReadyToHandleTransition) {
+ listeners.forEach { it.onTransitionFinished() }
+ }
+ isTransitionRunning = false
+ lastTransitionProgress = PROGRESS_UNSET
+ }
+
+ companion object {
+ private const val PROGRESS_UNSET = -1f
+ }
+}