Merge "Add initial unfold animation prototype under feature flag" into sc-v2-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
f6357101ac
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -47,6 +47,7 @@ android_library {
|
||||
|
||||
static_libs: [
|
||||
"PluginCoreLib",
|
||||
"androidx.dynamicanimation_dynamicanimation",
|
||||
],
|
||||
java_version: "1.8",
|
||||
min_sdk_version: "26",
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>) {
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user