1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.unfold 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.annotation.BinderThread 23 import android.os.SystemProperties 24 import android.util.Log 25 import android.view.View 26 import android.view.animation.DecelerateInterpolator 27 import com.android.app.tracing.TraceUtils.traceAsync 28 import com.android.internal.foldables.FoldLockSettingAvailabilityProvider 29 import com.android.internal.jank.Cuj.CUJ_FOLD_ANIM 30 import com.android.internal.jank.InteractionJankMonitor 31 import com.android.systemui.dagger.qualifiers.Background 32 import com.android.systemui.display.data.repository.DeviceStateRepository 33 import com.android.systemui.power.domain.interactor.PowerInteractor 34 import com.android.systemui.power.shared.model.ScreenPowerState 35 import com.android.systemui.statusbar.LinearSideLightRevealEffect 36 import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE 37 import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT 38 import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation 39 import com.android.systemui.unfold.dagger.UnfoldBg 40 import com.android.systemui.util.animation.data.repository.AnimationStatusRepository 41 import com.android.systemui.util.kotlin.race 42 import javax.inject.Inject 43 import kotlin.coroutines.resume 44 import kotlinx.coroutines.CompletableDeferred 45 import kotlinx.coroutines.CoroutineDispatcher 46 import kotlinx.coroutines.CoroutineScope 47 import kotlinx.coroutines.TimeoutCancellationException 48 import kotlinx.coroutines.flow.Flow 49 import kotlinx.coroutines.flow.catch 50 import kotlinx.coroutines.flow.distinctUntilChanged 51 import kotlinx.coroutines.flow.filter 52 import kotlinx.coroutines.flow.first 53 import kotlinx.coroutines.flow.flatMapLatest 54 import kotlinx.coroutines.flow.flow 55 import kotlinx.coroutines.flow.map 56 import kotlinx.coroutines.flow.onCompletion 57 import com.android.app.tracing.coroutines.launchTraced as launch 58 import kotlinx.coroutines.suspendCancellableCoroutine 59 import kotlinx.coroutines.withTimeout 60 61 class FoldLightRevealOverlayAnimation 62 @Inject 63 constructor( 64 @UnfoldBg private val bgDispatcher: CoroutineDispatcher, 65 private val deviceStateRepository: DeviceStateRepository, 66 private val powerInteractor: PowerInteractor, 67 @Background private val applicationScope: CoroutineScope, 68 private val animationStatusRepository: AnimationStatusRepository, 69 private val controllerFactory: FullscreenLightRevealAnimationController.Factory, 70 private val foldLockSettingAvailabilityProvider: FoldLockSettingAvailabilityProvider, 71 private val interactionJankMonitor: InteractionJankMonitor 72 ) : FullscreenLightRevealAnimation { 73 74 private val revealProgressValueAnimator: ValueAnimator = 75 ValueAnimator.ofFloat(ALPHA_OPAQUE, ALPHA_TRANSPARENT) 76 private val areAnimationEnabled: Flow<Boolean> 77 get() = animationStatusRepository.areAnimationsEnabled() 78 79 private lateinit var controller: FullscreenLightRevealAnimationController 80 @Volatile private var readyCallback: CompletableDeferred<Runnable>? = null 81 82 override fun init() { 83 // This method will be called only on devices where this animation is enabled, 84 // so normally this thread won't be created 85 if (!foldLockSettingAvailabilityProvider.isFoldLockBehaviorAvailable) { 86 return 87 } 88 89 controller = 90 controllerFactory.create( 91 displaySelector = { minByOrNull { it.naturalWidth } }, 92 effectFactory = { LinearSideLightRevealEffect(it.isVerticalRotation()) }, 93 overlayContainerName = OVERLAY_TITLE 94 ) 95 controller.init() 96 97 applicationScope.launch(context = bgDispatcher) { 98 powerInteractor.screenPowerState.collect { 99 if (it == ScreenPowerState.SCREEN_ON) { 100 readyCallback = null 101 } 102 } 103 } 104 105 applicationScope.launch(context = bgDispatcher) { 106 deviceStateRepository.state 107 .map { it == DeviceStateRepository.DeviceState.FOLDED } 108 .distinctUntilChanged() 109 .flatMapLatest { isFolded -> 110 flow<Nothing> { 111 if (!areAnimationEnabled.first() || !isFolded) { 112 return@flow 113 } 114 race( 115 { 116 traceAsync(TAG, "prepareAndPlayFoldAnimation()") { 117 withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) { 118 readyCallback = CompletableDeferred() 119 val onReady = readyCallback?.await() 120 readyCallback = null 121 controller.addOverlay(ALPHA_OPAQUE, onReady) 122 waitForScreenTurnedOn() 123 } 124 playFoldLightRevealOverlayAnimation() 125 } 126 }, 127 { waitForGoToSleep() } 128 ) 129 } 130 .catchTimeoutAndLog() 131 .onCompletion { 132 controller.ensureOverlayRemoved() 133 val onReady = readyCallback?.takeIf { it.isCompleted }?.getCompleted() 134 onReady?.run() 135 readyCallback = null 136 } 137 } 138 .collect {} 139 } 140 } 141 142 @BinderThread 143 override fun onScreenTurningOn(onOverlayReady: Runnable) { 144 readyCallback?.complete(onOverlayReady) ?: onOverlayReady.run() 145 } 146 147 private suspend fun waitForScreenTurnedOn() = 148 traceAsync(TAG, "waitForScreenTurnedOn()") { 149 powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() 150 } 151 152 private suspend fun waitForGoToSleep() = 153 traceAsync(TAG, "waitForGoToSleep()") { powerInteractor.isAsleep.filter { it }.first() } 154 155 private suspend fun playFoldLightRevealOverlayAnimation() = 156 trackCuj(CUJ_FOLD_ANIM, controller.scrimView) { 157 revealProgressValueAnimator.duration = ANIMATION_DURATION 158 revealProgressValueAnimator.interpolator = DecelerateInterpolator() 159 revealProgressValueAnimator.addUpdateListener { animation -> 160 controller.updateRevealAmount(animation.animatedFraction) 161 } 162 revealProgressValueAnimator.startAndAwaitCompletion() 163 } 164 165 private suspend fun trackCuj(cuj: Int, view: View?, block: suspend () -> Unit) { 166 view?.let { interactionJankMonitor.begin(it, cuj) } 167 try { 168 block() 169 } finally { 170 if (view != null) interactionJankMonitor.end(cuj) 171 } 172 } 173 174 private suspend fun ValueAnimator.startAndAwaitCompletion(): Unit = 175 suspendCancellableCoroutine { continuation -> 176 val listener = 177 object : AnimatorListenerAdapter() { 178 override fun onAnimationEnd(animation: Animator) { 179 continuation.resume(Unit) 180 removeListener(this) 181 } 182 } 183 addListener(listener) 184 continuation.invokeOnCancellation { removeListener(listener) } 185 start() 186 } 187 188 private fun <T> Flow<T>.catchTimeoutAndLog() = catch { exception -> 189 when (exception) { 190 is TimeoutCancellationException -> Log.e(TAG, "Fold light reveal animation timed out") 191 else -> throw exception 192 } 193 } 194 195 private companion object { 196 const val TAG = "FoldLightRevealOverlayAnimation" 197 const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L 198 const val OVERLAY_TITLE = "fold-animation-overlay" 199 val ANIMATION_DURATION: Long 200 get() = SystemProperties.getLong("persist.fold_animation_duration", 200L) 201 } 202 } 203