Merge "Add initial unfold animation prototype under feature flag" into sc-v2-dev

This commit is contained in:
TreeHugger Robot
2021-07-22 10:41:30 +00:00
committed by Android (Google) Code Review
24 changed files with 1072 additions and 0 deletions

View File

@@ -657,6 +657,9 @@
<!-- Indicate the display area rect for foldable devices in folded state. -->
<string name="config_foldedArea"></string>
<!-- Indicates whether to enable an animation when unfolding a device or not -->
<bool name="config_unfoldTransitionEnabled">false</bool>
<!-- Indicates that the device supports having more than one internal display on at the same
time. Only applicable to devices with more than one internal display. If this option is
set to false, DisplayManager will make additional effort to ensure no more than 1 internal

View File

@@ -3837,6 +3837,7 @@
<java-symbol type="array" name="config_foldedDeviceStates" />
<java-symbol type="string" name="config_foldedArea" />
<java-symbol type="bool" name="config_supportsConcurrentInternalDisplays" />
<java-symbol type="bool" name="config_unfoldTransitionEnabled" />
<java-symbol type="array" name="config_disableApksUnlessMatchedSku_apk_list" />
<java-symbol type="array" name="config_disableApkUnlessMatchedSku_skus_list" />

View File

@@ -47,6 +47,7 @@ android_library {
static_libs: [
"PluginCoreLib",
"androidx.dynamicanimation_dynamicanimation",
],
java_version: "1.8",
min_sdk_version: "26",

View File

@@ -0,0 +1,77 @@
/*
* 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.unfold
import android.content.Context
import android.hardware.SensorManager
import android.hardware.devicestate.DeviceStateManager
import android.os.Handler
import com.android.unfold.updates.screen.ScreenStatusProvider
import com.android.unfold.config.ANIMATION_MODE_HINGE_ANGLE
import com.android.unfold.config.ResourceUnfoldTransitionConfig
import com.android.unfold.config.UnfoldTransitionConfig
import com.android.unfold.progress.FixedTimingTransitionProgressProvider
import com.android.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider
import com.android.unfold.updates.DeviceFoldStateProvider
import com.android.unfold.updates.hinge.EmptyHingeAngleProvider
import com.android.unfold.updates.hinge.RotationSensorHingeAngleProvider
import java.lang.IllegalStateException
import java.util.concurrent.Executor
fun createUnfoldTransitionProgressProvider(
context: Context,
config: UnfoldTransitionConfig,
screenStatusProvider: ScreenStatusProvider,
deviceStateManager: DeviceStateManager,
sensorManager: SensorManager,
mainHandler: Handler,
mainExecutor: Executor
): UnfoldTransitionProgressProvider {
if (!config.isEnabled) {
throw IllegalStateException("Trying to create " +
"UnfoldTransitionProgressProvider when the transition is disabled")
}
val hingeAngleProvider =
if (config.mode == ANIMATION_MODE_HINGE_ANGLE) {
RotationSensorHingeAngleProvider(sensorManager)
} else {
EmptyHingeAngleProvider()
}
val foldStateProvider = DeviceFoldStateProvider(
context,
hingeAngleProvider,
screenStatusProvider,
deviceStateManager,
mainExecutor
)
return if (config.mode == ANIMATION_MODE_HINGE_ANGLE) {
PhysicsBasedUnfoldTransitionProgressProvider(
mainHandler,
foldStateProvider
)
} else {
FixedTimingTransitionProgressProvider(foldStateProvider)
}
}
fun createConfig(context: Context): UnfoldTransitionConfig =
ResourceUnfoldTransitionConfig(context)

View File

@@ -0,0 +1,38 @@
/*
* 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.unfold
import android.annotation.FloatRange
import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.statusbar.policy.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)
}
}

View File

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

View File

@@ -0,0 +1,38 @@
/*
* 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.unfold.config
import android.annotation.IntDef
interface UnfoldTransitionConfig {
val isEnabled: Boolean
@AnimationMode
val mode: Int
}
@IntDef(prefix = ["ANIMATION_MODE_"], value = [
ANIMATION_MODE_DISABLED,
ANIMATION_MODE_FIXED_TIMING,
ANIMATION_MODE_HINGE_ANGLE
])
@Retention(AnnotationRetention.SOURCE)
annotation class AnimationMode
const val ANIMATION_MODE_DISABLED = 0
const val ANIMATION_MODE_FIXED_TIMING = 1
const val ANIMATION_MODE_HINGE_ANGLE = 2

View File

@@ -0,0 +1,117 @@
/*
* 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.unfold.progress
import android.animation.Animator
import android.animation.ObjectAnimator
import android.util.FloatProperty
import com.android.unfold.UnfoldTransitionProgressProvider
import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
import com.android.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
import com.android.unfold.updates.FoldStateProvider
import com.android.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
}
}

View File

@@ -0,0 +1,184 @@
/*
* 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.unfold.progress
import android.os.Handler
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.unfold.UnfoldTransitionProgressProvider
import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
import com.android.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
import com.android.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
import com.android.unfold.updates.FoldStateProvider
import com.android.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.unfold.updates.FoldStateProvider.FoldUpdatesListener
/**
* Maps fold updates to unfold transition progress using DynamicAnimation.
*
* TODO(b/193793338) Current limitations:
* - doesn't handle folding transition
* - doesn't handle postures
*/
internal class PhysicsBasedUnfoldTransitionProgressProvider(
private val handler: Handler,
private val foldStateProvider: FoldStateProvider
) :
UnfoldTransitionProgressProvider,
FoldUpdatesListener,
DynamicAnimation.OnAnimationEndListener {
private val springAnimation = SpringAnimation(this, AnimationProgressProperty)
.apply {
addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider)
}
private val timeoutRunnable = TimeoutRunnable()
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
springAnimation.animateToFinalPosition(angle / 180f)
}
override fun onFoldUpdate(@FoldUpdate update: Int) {
when (update) {
FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> {
onStartTransition()
startTransition(startValue = 0f)
}
FOLD_UPDATE_FINISH_FULL_OPEN -> {
cancelTransition(endValue = 1f, animate = true)
}
FOLD_UPDATE_FINISH_CLOSED -> {
cancelTransition(endValue = 0f, animate = false)
}
}
}
private fun cancelTransition(endValue: Float, animate: Boolean) {
handler.removeCallbacks(timeoutRunnable)
if (animate) {
isAnimatedCancelRunning = true
springAnimation.animateToFinalPosition(endValue)
} else {
transitionProgress = endValue
isAnimatedCancelRunning = false
isTransitionRunning = false
springAnimation.cancel()
listeners.forEach {
it.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
}
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()
handler.postDelayed(timeoutRunnable, TRANSITION_TIMEOUT_MILLIS)
}
override fun addCallback(listener: TransitionProgressListener) {
listeners.add(listener)
}
override fun removeCallback(listener: TransitionProgressListener) {
listeners.remove(listener)
}
private inner class TimeoutRunnable : Runnable {
override fun run() {
cancelTransition(endValue = 1f, animate = true)
}
}
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 TRANSITION_TIMEOUT_MILLIS = 2000L
private const val SPRING_STIFFNESS = 200.0f
private const val MINIMAL_VISIBLE_CHANGE = 0.001f

View File

@@ -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.unfold.updates
import android.content.Context
import android.hardware.devicestate.DeviceStateManager
import androidx.core.util.Consumer
import com.android.unfold.updates.screen.ScreenStatusProvider
import com.android.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.unfold.updates.FoldStateProvider.FoldUpdatesListener
import com.android.unfold.updates.hinge.FULLY_OPEN_DEGREES
import com.android.unfold.updates.hinge.HingeAngleProvider
import java.util.concurrent.Executor
internal class DeviceFoldStateProvider(
context: Context,
private val hingeAngleProvider: HingeAngleProvider,
private val screenStatusProvider: ScreenStatusProvider,
private val deviceStateManager: DeviceStateManager,
private val mainExecutor: Executor
) : FoldStateProvider {
private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
@FoldUpdate
private var lastFoldUpdate: Int? = null
private val hingeAngleListener = HingeAngleListener()
private val screenListener = ScreenStatusListener()
private val foldStateListener = FoldStateListener(context)
private var isFolded = false
override fun start() {
deviceStateManager.registerCallback(
mainExecutor,
foldStateListener
)
screenStatusProvider.addCallback(screenListener)
hingeAngleProvider.addCallback(hingeAngleListener)
}
override fun stop() {
screenStatusProvider.removeCallback(screenListener)
deviceStateManager.unregisterCallback(foldStateListener)
hingeAngleProvider.removeCallback(hingeAngleListener)
hingeAngleProvider.stop()
}
override fun addCallback(listener: FoldUpdatesListener) {
outputListeners.add(listener)
}
override fun removeCallback(listener: FoldUpdatesListener) {
outputListeners.remove(listener)
}
private fun onHingeAngle(angle: Float) {
when (lastFoldUpdate) {
FOLD_UPDATE_FINISH_FULL_OPEN -> {
if (FULLY_OPEN_DEGREES - angle > MOVEMENT_THRESHOLD_DEGREES) {
lastFoldUpdate = FOLD_UPDATE_START_CLOSING
outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_CLOSING) }
}
}
FOLD_UPDATE_START_OPENING, FOLD_UPDATE_START_CLOSING -> {
if (FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES) {
lastFoldUpdate = FOLD_UPDATE_FINISH_FULL_OPEN
outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) }
}
}
}
outputListeners.forEach { it.onHingeAngleUpdate(angle) }
}
private inner class FoldStateListener(context: Context) :
DeviceStateManager.FoldStateListener(context, { folded: Boolean ->
isFolded = folded
if (folded) {
lastFoldUpdate = FOLD_UPDATE_FINISH_CLOSED
outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) }
hingeAngleProvider.stop()
} else {
lastFoldUpdate = FOLD_UPDATE_START_OPENING
outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_OPENING) }
hingeAngleProvider.start()
}
})
private inner class ScreenStatusListener :
ScreenStatusProvider.ScreenListener {
override fun onScreenTurnedOn() {
if (!isFolded) {
outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) }
}
}
}
private inner class HingeAngleListener : Consumer<Float> {
override fun accept(angle: Float) {
onHingeAngle(angle)
}
}
}
private const val MOVEMENT_THRESHOLD_DEGREES = 10f
private const val FULLY_OPEN_THRESHOLD_DEGREES = 10f

View File

@@ -0,0 +1,55 @@
/*
* 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.unfold.updates
import android.annotation.FloatRange
import android.annotation.IntDef
import com.android.unfold.updates.FoldStateProvider.FoldUpdatesListener
import com.android.systemui.statusbar.policy.CallbackController
/**
* Allows to subscribe to main events related to fold/unfold process such as hinge angle update,
* start folding/unfolding, screen availability
*/
internal interface FoldStateProvider : CallbackController<FoldUpdatesListener> {
fun start()
fun stop()
interface FoldUpdatesListener {
fun onHingeAngleUpdate(@FloatRange(from = 0.0, to = 180.0) angle: Float)
fun onFoldUpdate(@FoldUpdate update: Int)
}
@IntDef(prefix = ["FOLD_UPDATE_"], value = [
FOLD_UPDATE_START_OPENING,
FOLD_UPDATE_HALF_OPEN,
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_HALF_OPEN = 1
const val FOLD_UPDATE_START_CLOSING = 2
const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 3
const val FOLD_UPDATE_FINISH_HALF_OPEN = 4
const val FOLD_UPDATE_FINISH_FULL_OPEN = 5
const val FOLD_UPDATE_FINISH_CLOSED = 6

View File

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

View File

@@ -0,0 +1,12 @@
package com.android.unfold.updates.hinge
import androidx.core.util.Consumer
import com.android.systemui.statusbar.policy.CallbackController
internal interface HingeAngleProvider : CallbackController<Consumer<Float>> {
fun start()
fun stop()
}
const val FULLY_OPEN_DEGREES = 180f
const val FULLY_CLOSED_DEGREES = 0f

View File

@@ -0,0 +1,67 @@
package com.android.unfold.updates.hinge
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.core.util.Consumer
import com.android.systemui.shared.recents.utilities.Utilities
/**
* Temporary hinge angle provider that uses rotation sensor instead.
* It requires to have the device in a certain position to work correctly
* (flat to the ground)
*/
internal class RotationSensorHingeAngleProvider(
private val sensorManager: SensorManager
) : HingeAngleProvider {
private val sensorListener = HingeAngleSensorListener()
private val listeners: MutableList<Consumer<Float>> = arrayListOf()
override fun start() {
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR)
sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_FASTEST)
}
override fun stop() {
sensorManager.unregisterListener(sensorListener)
}
override fun removeCallback(listener: Consumer<Float>) {
listeners.remove(listener)
}
override fun addCallback(listener: Consumer<Float>) {
listeners.add(listener)
}
private fun onHingeAngle(angle: Float) {
listeners.forEach { it.accept(angle) }
}
private inner class HingeAngleSensorListener : SensorEventListener {
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
override fun onSensorChanged(event: SensorEvent) {
// Jumbojack sends incorrect sensor reading 1.0f event in the beginning, let's ignore it
if (event.values[3] == 1.0f) return
val angleRadians = event.values.convertToAngle()
val hingeAngleDegrees = Math.toDegrees(angleRadians).toFloat()
val angle = Utilities.clamp(hingeAngleDegrees, FULLY_CLOSED_DEGREES, FULLY_OPEN_DEGREES)
onHingeAngle(angle)
}
private val rotationMatrix = FloatArray(9)
private val resultOrientation = FloatArray(9)
private fun FloatArray.convertToAngle(): Double {
SensorManager.getRotationMatrixFromVector(rotationMatrix, this)
SensorManager.getOrientation(rotationMatrix, resultOrientation)
return resultOrientation[2] + Math.PI
}
}
}

View File

@@ -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.unfold.updates.screen
import com.android.unfold.updates.screen.ScreenStatusProvider.ScreenListener
import com.android.systemui.statusbar.policy.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()
}
}

View File

@@ -24,6 +24,8 @@ import android.app.INotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.om.OverlayManager;
import android.hardware.SensorManager;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.display.AmbientDisplayConfiguration;
import android.hardware.display.ColorDisplayManager;
import android.os.Handler;
@@ -59,6 +61,7 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.doze.AlwaysOnDisplayPolicy;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.LifecycleScreenStatusProvider;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarA11yHelper;
import com.android.systemui.navigationbar.NavigationBarController;
@@ -77,6 +80,9 @@ import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.shared.system.WindowManagerWrapper;
import com.android.unfold.UnfoldTransitionFactory;
import com.android.unfold.UnfoldTransitionProgressProvider;
import com.android.unfold.config.UnfoldTransitionConfig;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -375,6 +381,37 @@ public class DependencyProvider {
return WindowManagerWrapper.getInstance();
}
/** */
@Provides
@SysUISingleton
public UnfoldTransitionProgressProvider provideUnfoldTransitionProgressProvider(
Context context,
UnfoldTransitionConfig config,
LifecycleScreenStatusProvider screenStatusProvider,
DeviceStateManager deviceStateManager,
SensorManager sensorManager,
@Main Executor executor,
@Main Handler handler
) {
return UnfoldTransitionFactory
.createUnfoldTransitionProgressProvider(
context,
config,
screenStatusProvider,
deviceStateManager,
sensorManager,
handler,
executor
);
}
/** */
@Provides
@SysUISingleton
public UnfoldTransitionConfig provideUnfoldTransitionConfig(Context context) {
return UnfoldTransitionFactory.createConfig(context);
}
/** */
@Provides
@SysUISingleton

View File

@@ -41,6 +41,7 @@ import android.content.pm.ShortcutManager;
import android.content.res.Resources;
import android.hardware.SensorManager;
import android.hardware.SensorPrivacyManager;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.display.ColorDisplayManager;
import android.hardware.display.DisplayManager;
import android.hardware.face.FaceManager;
@@ -157,6 +158,12 @@ public class FrameworkServicesModule {
return context.getSystemService(DisplayManager.class);
}
@Provides
@Singleton
static DeviceStateManager provideDeviceStateManager(Context context) {
return context.getSystemService(DeviceStateManager.class);
}
@Provides
@Singleton
static IActivityManager provideIActivityManager() {

View File

@@ -0,0 +1,44 @@
/*
* 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.keyguard
import com.android.systemui.dagger.SysUISingleton
import com.android.unfold.updates.screen.ScreenStatusProvider
import com.android.unfold.updates.screen.ScreenStatusProvider.ScreenListener
import javax.inject.Inject
@SysUISingleton
class LifecycleScreenStatusProvider @Inject constructor(screenLifecycle: ScreenLifecycle) :
ScreenStatusProvider, ScreenLifecycle.Observer {
init {
screenLifecycle.addObserver(this)
}
private val listeners: MutableList<ScreenListener> = mutableListOf()
override fun removeCallback(listener: ScreenListener) {
listeners.remove(listener)
}
override fun addCallback(listener: ScreenListener) {
listeners.add(listener)
}
override fun onScreenTurnedOn() {
listeners.forEach(ScreenListener::onScreenTurnedOn)
}
}

View File

@@ -83,6 +83,35 @@ object LiftReveal : LightRevealEffect {
}
}
class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
private val INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN_REVERSE
override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
val interpolatedAmount = INTERPOLATOR.getInterpolation(amount)
// TODO(b/193801466): add alpha reveal in the beginning as well
scrim.revealGradientEndColorAlpha =
1f - LightRevealEffect.getPercentPastThreshold(interpolatedAmount, threshold = 0.6f)
if (isVertical) {
scrim.setRevealGradientBounds(
left = scrim.width / 2 - (scrim.width / 2) * interpolatedAmount,
top = 0f,
right = scrim.width / 2 + (scrim.width / 2) * interpolatedAmount,
bottom = scrim.height.toFloat()
)
} else {
scrim.setRevealGradientBounds(
left = 0f,
top = scrim.height / 2 - (scrim.height / 2) * interpolatedAmount,
right = scrim.width.toFloat(),
bottom = scrim.height / 2 + (scrim.height / 2) * interpolatedAmount
)
}
}
}
class CircleReveal(
/** X-value of the circle center of the reveal. */
val centerX: Float,

View File

@@ -187,6 +187,7 @@ import com.android.systemui.recents.ScreenPinningRequest;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.settings.brightness.BrightnessSlider;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.unfold.config.UnfoldTransitionConfig;
import com.android.systemui.statusbar.AutoHideUiElement;
import com.android.systemui.statusbar.BackDropView;
import com.android.systemui.statusbar.CircleReveal;
@@ -245,6 +246,7 @@ import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
import com.android.systemui.volume.VolumeComponent;
import com.android.systemui.wmshell.BubblesManager;
import com.android.wm.shell.bubbles.Bubbles;
@@ -467,6 +469,8 @@ public class StatusBar extends SystemUI implements DemoMode,
protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
private final BrightnessSlider.Factory mBrightnessSliderFactory;
private final FeatureFlags mFeatureFlags;
private final UnfoldTransitionConfig mUnfoldTransitionConfig;
private final Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealOverlayAnimation;
private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
@@ -801,6 +805,8 @@ public class StatusBar extends SystemUI implements DemoMode,
NotificationIconAreaController notificationIconAreaController,
BrightnessSlider.Factory brightnessSliderFactory,
WiredChargingRippleController chargingRippleAnimationController,
UnfoldTransitionConfig unfoldTransitionConfig,
Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation,
OngoingCallController ongoingCallController,
SystemStatusAnimationScheduler animationScheduler,
StatusBarLocationPublisher locationPublisher,
@@ -887,6 +893,8 @@ public class StatusBar extends SystemUI implements DemoMode,
mNotificationIconAreaController = notificationIconAreaController;
mBrightnessSliderFactory = brightnessSliderFactory;
mChargingRippleAnimationController = chargingRippleAnimationController;
mUnfoldTransitionConfig = unfoldTransitionConfig;
mUnfoldLightRevealOverlayAnimation = unfoldLightRevealOverlayAnimation;
mOngoingCallController = ongoingCallController;
mAnimationScheduler = animationScheduler;
mStatusBarLocationPublisher = locationPublisher;
@@ -1058,6 +1066,10 @@ public class StatusBar extends SystemUI implements DemoMode,
mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener);
if (mUnfoldTransitionConfig.isEnabled()) {
mUnfoldLightRevealOverlayAnimation.get().init();
}
mPluginManager.addPluginListener(
new PluginListener<OverlayPlugin>() {
private ArraySet<OverlayPlugin> mOverlays = new ArraySet<>();

View File

@@ -48,6 +48,7 @@ import com.android.systemui.plugins.PluginDependencyProvider;
import com.android.systemui.recents.ScreenPinningRequest;
import com.android.systemui.settings.brightness.BrightnessSlider;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.unfold.config.UnfoldTransitionConfig;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -105,6 +106,7 @@ import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
import com.android.systemui.volume.VolumeComponent;
import com.android.systemui.wmshell.BubblesManager;
import com.android.wm.shell.bubbles.Bubbles;
@@ -213,6 +215,8 @@ public interface StatusBarPhoneModule {
NotificationIconAreaController notificationIconAreaController,
BrightnessSlider.Factory brightnessSliderFactory,
WiredChargingRippleController chargingRippleAnimationController,
UnfoldTransitionConfig unfoldTransitionConfig,
Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation,
OngoingCallController ongoingCallController,
SystemStatusAnimationScheduler animationScheduler,
StatusBarLocationPublisher locationPublisher,
@@ -302,6 +306,8 @@ public interface StatusBarPhoneModule {
notificationIconAreaController,
brightnessSliderFactory,
chargingRippleAnimationController,
unfoldTransitionConfig,
unfoldLightRevealOverlayAnimation,
ongoingCallController,
animationScheduler,
locationPublisher,

View File

@@ -0,0 +1,127 @@
/*
* 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 android.content.Context
import android.graphics.PixelFormat
import android.hardware.devicestate.DeviceStateManager
import android.hardware.devicestate.DeviceStateManager.FoldStateListener
import android.view.Surface
import android.view.WindowManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.unfold.UnfoldTransitionProgressProvider
import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
import java.util.concurrent.Executor
import java.util.function.Consumer
import javax.inject.Inject
@SysUISingleton
class UnfoldLightRevealOverlayAnimation @Inject constructor(
private val context: Context,
private val deviceStateManager: DeviceStateManager,
private val unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
@Main private val executor: Executor,
private val windowManager: WindowManager
) {
private val transitionListener = TransitionListener()
private var scrimView: LightRevealScrim? = null
fun init() {
deviceStateManager.registerCallback(executor, FoldListener())
unfoldTransitionProgressProvider.addCallback(transitionListener)
}
private inner class TransitionListener : TransitionProgressListener {
override fun onTransitionProgress(progress: Float) {
scrimView?.revealAmount = progress
}
override fun onTransitionFinished() {
removeOverlayView()
}
override fun onTransitionStarted() {
}
}
private inner class FoldListener : FoldStateListener(context, Consumer { isFolded ->
if (isFolded) {
removeOverlayView()
} else {
// Add overlay view before starting the transition as soon as we unfolded the device
addOverlayView()
}
})
private fun addOverlayView() {
val params: WindowManager.LayoutParams = WindowManager.LayoutParams()
params.height = WindowManager.LayoutParams.MATCH_PARENT
params.width = WindowManager.LayoutParams.MATCH_PARENT
params.format = PixelFormat.TRANSLUCENT
// TODO(b/193801466): create a separate type for this overlay
params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
params.title = "Unfold Light Reveal Animation"
params.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
params.fitInsetsTypes = 0
params.flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
params.setTrustedOverlay()
val rotation = windowManager.defaultDisplay.rotation
val isVerticalFold = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
val newScrimView = LightRevealScrim(context, null)
.apply {
revealEffect = LinearLightRevealEffect(isVerticalFold)
revealAmountListener = Consumer {}
revealAmount = 0f
}
val packageName: String = newScrimView.context.opPackageName
params.packageName = packageName
params.hideTimeoutMilliseconds = OVERLAY_HIDE_TIMEOUT_MILLIS
if (scrimView?.parent != null) {
windowManager.removeView(scrimView)
}
this.scrimView = newScrimView
try {
windowManager.addView(scrimView, params)
} catch (e: WindowManager.BadTokenException) {
e.printStackTrace()
}
}
private fun removeOverlayView() {
scrimView?.let {
if (it.parent != null) {
windowManager.removeViewImmediate(it)
}
scrimView = null
}
}
}
private const val OVERLAY_HIDE_TIMEOUT_MILLIS = 10_000L

View File

@@ -98,6 +98,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.ScreenPinningRequest;
import com.android.systemui.settings.brightness.BrightnessSlider;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.unfold.config.UnfoldTransitionConfig;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -146,6 +147,7 @@ import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.volume.VolumeComponent;
@@ -266,6 +268,8 @@ public class StatusBarTest extends SysuiTestCase {
@Mock private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
@Mock private BrightnessSlider.Factory mBrightnessSliderFactory;
@Mock private WiredChargingRippleController mWiredChargingRippleController;
@Mock private UnfoldTransitionConfig mUnfoldTransitionConfig;
@Mock private Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealOverlayAnimationLazy;
@Mock private OngoingCallController mOngoingCallController;
@Mock private SystemStatusAnimationScheduler mAnimationScheduler;
@Mock private StatusBarLocationPublisher mLocationPublisher;
@@ -439,6 +443,8 @@ public class StatusBarTest extends SysuiTestCase {
mNotificationIconAreaController,
mBrightnessSliderFactory,
mWiredChargingRippleController,
mUnfoldTransitionConfig,
mUnfoldLightRevealOverlayAnimationLazy,
mOngoingCallController,
mAnimationScheduler,
mLocationPublisher,