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
+    }
+}