1 /*
2  * Copyright (C) 2023 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 package com.android.quickstep.util.unfold
17 
18 import android.app.Activity
19 import android.os.Trace
20 import android.view.Surface
21 import com.android.launcher3.Alarm
22 import com.android.launcher3.DeviceProfile
23 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener
24 import com.android.launcher3.anim.PendingAnimation
25 import com.android.launcher3.config.FeatureFlags
26 import com.android.launcher3.uioverrides.QuickstepLauncher
27 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
28 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
29 
30 /** Controls animations that are happening during unfolding foldable devices */
31 class LauncherUnfoldTransitionController(
32     private val launcher: QuickstepLauncher,
33     private val progressProvider: ProxyUnfoldTransitionProvider
34 ) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener {
35 
36     private var isTablet: Boolean? = null
37     private var hasUnfoldTransitionStarted = false
38     private val timeoutAlarm =
<lambda>null39         Alarm().apply {
40             setOnAlarmListener {
41                 onTransitionFinished()
42                 Trace.endAsyncSection("$TAG#startedPreemptively", 0)
43             }
44         }
45 
46     init {
47         launcher.addOnDeviceProfileChangeListener(this)
48         launcher.registerActivityLifecycleCallbacks(this)
49     }
50 
onActivityPausednull51     override fun onActivityPaused(activity: Activity) {
52         progressProvider.removeCallback(this)
53     }
54 
onActivityResumednull55     override fun onActivityResumed(activity: Activity) {
56         progressProvider.addCallback(this)
57     }
58 
onDeviceProfileChangednull59     override fun onDeviceProfileChanged(dp: DeviceProfile) {
60         if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
61             return
62         }
63 
64         if (isTablet != null && dp.isTablet != isTablet) {
65             // We should preemptively start the animation only if:
66             // - We changed to the unfolded screen
67             // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't
68             //   receive transition progress events from SystemUI later because there was no
69             //   IPC connection established (e.g. because of SystemUI crash)
70             // - SystemUI has not already sent unfold animation progress events. This might happen
71             //   if Launcher was not open during unfold, in this case we receive the configuration
72             //   change only after we went back to home screen and we don't want to start the
73             //   animation in this case.
74             if (dp.isTablet && progressProvider.isActive && !hasUnfoldTransitionStarted) {
75                 // Preemptively start the unfold animation to make sure that we have drawn
76                 // the first frame of the animation before the screen gets unblocked
77                 onTransitionStarted()
78                 Trace.beginAsyncSection("$TAG#startedPreemptively", 0)
79                 timeoutAlarm.setAlarm(PREEMPTIVE_UNFOLD_TIMEOUT_MS)
80             }
81             if (!dp.isTablet) {
82                 // Reset unfold transition status when folded
83                 hasUnfoldTransitionStarted = false
84             }
85         }
86 
87         isTablet = dp.isTablet
88     }
89 
onTransitionStartednull90     override fun onTransitionStarted() {
91         hasUnfoldTransitionStarted = true
92         launcher.animationCoordinator.setAnimation(
93             provider = this,
94             factory = this::onPrepareUnfoldAnimation,
95             duration =
96                 1000L // The expected duration for the animation. Then only comes to play if we have
97             // to run the animation ourselves in case sysui misses the end signal
98         )
99         timeoutAlarm.cancelAlarm()
100     }
101 
onTransitionProgressnull102     override fun onTransitionProgress(progress: Float) {
103         hasUnfoldTransitionStarted = true
104         launcher.animationCoordinator.getPlaybackController(this)?.setPlayFraction(progress)
105     }
106 
onTransitionFinishednull107     override fun onTransitionFinished() {
108         // Run the animation to end the animation in case it is not already at end progress. It
109         // will scale the duration to the remaining progress
110         launcher.animationCoordinator.getPlaybackController(this)?.start()
111         timeoutAlarm.cancelAlarm()
112     }
113 
onPrepareUnfoldAnimationnull114     private fun onPrepareUnfoldAnimation(anim: PendingAnimation) {
115         val dp = launcher.deviceProfile
116         val rotation = dp.displayInfo.rotation
117         val isVertical = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
118         UnfoldAnimationBuilder.buildUnfoldAnimation(
119             launcher,
120             isVertical,
121             dp.displayInfo.currentSize,
122             anim
123         )
124     }
125 
126     companion object {
127         private const val TAG = "LauncherUnfoldTransitionController"
128     }
129 }
130