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