Merge cherrypicks of ['googleplex-android-review.googlesource.com/31154470', 'googleplex-android-review.googlesource.com/31195717', 'googleplex-android-review.googlesource.com/31014284', 'googleplex-android-review.googlesource.com/31395613', 'googleplex-android-review.googlesource.com/31569659'] into 25Q1-release.

Change-Id: I446aae829c6c9c29b5d7808787dba8f1b68c361b
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 5eadd06..221beaf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2951,9 +2951,11 @@
             final int transitType = info.getType();
             TransitionInfo.Change pipChange = null;
             int closingSplitTaskId = -1;
-            // This array tracks if we are sending stages TO_BACK in this transition.
-            // TODO (b/349828130): Update for n apps
-            boolean[] stagesSentToBack = new boolean[2];
+            // This array tracks where we are sending stages (TO_BACK/TO_FRONT) in this transition.
+            // TODO (b/349828130): Update for n apps (needs to handle different indices than 0/1).
+            //  Also make sure having multiple changes per stage (2+ tasks in one stage) is being
+            //  handled properly.
+            int[] stageChanges = new int[2];
 
             for (int iC = 0; iC < info.getChanges().size(); ++iC) {
                 final TransitionInfo.Change change = info.getChanges().get(iC);
@@ -3016,18 +3018,25 @@
                                 + " with " + taskId + " before startAnimation().");
                     }
                 }
-                if (isClosingType(change.getMode()) &&
-                        getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED) {
 
-                    // Record which stages are getting sent to back
-                    if (change.getMode() == TRANSIT_TO_BACK) {
-                        stagesSentToBack[getStageOfTask(taskId)] = true;
-                    }
-
+                final int stageOfTaskId = getStageOfTask(taskId);
+                if (stageOfTaskId == STAGE_TYPE_UNDEFINED) {
+                    continue;
+                }
+                if (isClosingType(change.getMode())) {
                     // (For PiP transitions) If either one of the 2 stages is closing we're assuming
                     // we'll break split
                     closingSplitTaskId = taskId;
                 }
+                if (transitType == WindowManager.TRANSIT_WAKE) {
+                    // Record which stages are receiving which changes
+                    if ((change.getMode() == TRANSIT_TO_BACK
+                            || change.getMode() == TRANSIT_TO_FRONT)
+                            && (stageOfTaskId == STAGE_TYPE_MAIN
+                            || stageOfTaskId == STAGE_TYPE_SIDE)) {
+                        stageChanges[stageOfTaskId] = change.getMode();
+                    }
+                }
             }
 
             if (pipChange != null) {
@@ -3052,19 +3061,11 @@
                 return true;
             }
 
-            // If keyguard is active, check to see if we have our TO_BACK transitions in order.
-            // This array should either be all false (no split stages sent to back) or all true
-            // (all stages sent to back). In any other case (which can happen with SHOW_ABOVE_LOCKED
-            // apps) we should break split.
-            if (mKeyguardActive) {
-                boolean isFirstStageSentToBack = stagesSentToBack[0];
-                for (boolean b : stagesSentToBack) {
-                    // Compare each boolean to the first one. If any are different, break split.
-                    if (b != isFirstStageSentToBack) {
-                        dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
-                        break;
-                    }
-                }
+            // If keyguard is active, check to see if we have all our stages showing. If one stage
+            // was moved but not the other (which can happen with SHOW_ABOVE_LOCKED apps), we should
+            // break split.
+            if (mKeyguardActive && stageChanges[STAGE_TYPE_MAIN] != stageChanges[STAGE_TYPE_SIDE]) {
+                dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
             }
 
             final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage();
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 83ca496..2b71c87 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -19,10 +19,11 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.Bundle;
+import android.os.IRemoteCallback;
 import android.view.MotionEvent;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
-// Next ID: 34
+// Next ID: 36
 oneway interface IOverviewProxy {
 
     void onActiveNavBarRegionChanges(in Region activeRegion) = 11;
@@ -137,4 +138,10 @@
      * Sent when {@link TaskbarDelegate#appTransitionPending} is called.
      */
     void appTransitionPending(boolean pending) = 34;
+
+    /**
+     * Sent right after OverviewProxy calls unbindService() on the TouchInteractionService.
+     * TouchInteractionService is expected to send the reply once it has finished cleaning up.
+     */
+    void onUnbind(IRemoteCallback reply) = 35;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index 2978595..9596a54 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -20,10 +20,12 @@
 import android.hardware.input.InputManager
 import android.hardware.input.KeyGestureEvent
 import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
 import androidx.datastore.preferences.core.MutablePreferences
 import androidx.datastore.preferences.core.PreferenceDataStoreFactory
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
 import androidx.datastore.preferences.core.intPreferencesKey
 import androidx.datastore.preferences.core.longPreferencesKey
 import androidx.datastore.preferences.preferencesDataStoreFile
@@ -68,7 +70,7 @@
 
     suspend fun updateGestureEduModel(
         gestureType: GestureType,
-        transform: (GestureEduModel) -> GestureEduModel
+        transform: (GestureEduModel) -> GestureEduModel,
     )
 
     suspend fun updateEduDeviceConnectionTime(
@@ -149,6 +151,8 @@
                         String.format(DATASTORE_DIR, userId)
                     )
                 },
+                corruptionHandler =
+                    ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
                 scope = newDsScope,
             )
         dataStoreScope = newDsScope
@@ -159,7 +163,7 @@
 
     private fun getGestureEduModel(
         gestureType: GestureType,
-        preferences: Preferences
+        preferences: Preferences,
     ): GestureEduModel {
         return GestureEduModel(
             signalCount = preferences[getSignalCountKey(gestureType)] ?: 0,
@@ -183,7 +187,7 @@
 
     override suspend fun updateGestureEduModel(
         gestureType: GestureType,
-        transform: (GestureEduModel) -> GestureEduModel
+        transform: (GestureEduModel) -> GestureEduModel,
     ) {
         datastore.filterNotNull().first().edit { preferences ->
             val currentModel = getGestureEduModel(gestureType, preferences)
@@ -193,17 +197,17 @@
             setInstant(
                 preferences,
                 updatedModel.lastShortcutTriggeredTime,
-                getLastShortcutTriggeredTimeKey(gestureType)
+                getLastShortcutTriggeredTimeKey(gestureType),
             )
             setInstant(
                 preferences,
                 updatedModel.usageSessionStartTime,
-                getUsageSessionStartTimeKey(gestureType)
+                getUsageSessionStartTimeKey(gestureType),
             )
             setInstant(
                 preferences,
                 updatedModel.lastEducationTime,
-                getLastEducationTimeKey(gestureType)
+                getLastEducationTimeKey(gestureType),
             )
         }
     }
@@ -220,12 +224,12 @@
             setInstant(
                 preferences,
                 updatedModel.keyboardFirstConnectionTime,
-                getKeyboardFirstConnectionTimeKey()
+                getKeyboardFirstConnectionTimeKey(),
             )
             setInstant(
                 preferences,
                 updatedModel.touchpadFirstConnectionTime,
-                getTouchpadFirstConnectionTimeKey()
+                getTouchpadFirstConnectionTimeKey(),
             )
         }
     }
@@ -235,7 +239,7 @@
             keyboardFirstConnectionTime =
                 preferences[getKeyboardFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
             touchpadFirstConnectionTime =
-                preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) }
+                preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
         )
     }
 
@@ -263,7 +267,7 @@
     private fun setInstant(
         preferences: MutablePreferences,
         instant: Instant?,
-        key: Preferences.Key<Long>
+        key: Preferences.Key<Long>,
     ) {
         if (instant != null) {
             // Use epochSecond because an instant is defined as a signed long (64bit number) of
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
index a89ec70..315d3b1 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
@@ -18,8 +18,10 @@
 
 import android.content.Context
 import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
 import androidx.datastore.preferences.core.longPreferencesKey
 import androidx.datastore.preferences.preferencesDataStore
 import com.android.systemui.dagger.SysUISingleton
@@ -45,7 +47,12 @@
     ) : this(applicationContext, backgroundScope, dataStoreName = DATASTORE_NAME)
 
     private val Context.dataStore: DataStore<Preferences> by
-        preferencesDataStore(name = dataStoreName, scope = backgroundScope)
+        preferencesDataStore(
+            name = dataStoreName,
+            corruptionHandler =
+                ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
+            scope = backgroundScope,
+        )
 
     suspend fun isLaunched(deviceType: DeviceType): Boolean = loadData()[deviceType]!!.isLaunched
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index a595d81..29bda76 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -86,15 +86,56 @@
             transition.values[SMARTSPACE_BOUNDS] = targetSSView.getRect()
         }
 
-        open fun mutateBounds(
-            view: View,
-            fromIsVis: Boolean,
-            toIsVis: Boolean,
-            fromBounds: Rect,
-            toBounds: Rect,
-            fromSSBounds: Rect?,
-            toSSBounds: Rect?,
-        ) {}
+        open fun initTargets(from: Target, to: Target) {}
+
+        open fun mutateTargets(from: Target, to: Target) {}
+
+        data class Target(
+            var view: View,
+            var visibility: Int,
+            var isVisible: Boolean,
+            var alpha: Float,
+            var bounds: Rect,
+            var ssBounds: Rect?,
+        ) {
+            companion object {
+                fun fromStart(startValues: TransitionValues): Target {
+                    var fromVis = startValues.values[PROP_VISIBILITY] as Int
+                    var fromIsVis = fromVis == View.VISIBLE
+                    var fromAlpha = startValues.values[PROP_ALPHA] as Float
+
+                    // Align starting visibility and alpha
+                    if (!fromIsVis) fromAlpha = 0f
+                    else if (fromAlpha <= 0f) {
+                        fromIsVis = false
+                        fromVis = View.INVISIBLE
+                    }
+
+                    return Target(
+                        view = startValues.view,
+                        visibility = fromVis,
+                        isVisible = fromIsVis,
+                        alpha = fromAlpha,
+                        bounds = startValues.values[PROP_BOUNDS] as Rect,
+                        ssBounds = startValues.values[SMARTSPACE_BOUNDS] as Rect?,
+                    )
+                }
+
+                fun fromEnd(endValues: TransitionValues): Target {
+                    val toVis = endValues.values[PROP_VISIBILITY] as Int
+                    val toIsVis = toVis == View.VISIBLE
+
+                    return Target(
+                        view = endValues.view,
+                        visibility = toVis,
+                        isVisible = toIsVis,
+                        alpha = if (toIsVis) 1f else 0f,
+                        bounds = endValues.values[PROP_BOUNDS] as Rect,
+                        ssBounds = endValues.values[SMARTSPACE_BOUNDS] as Rect?,
+                    )
+                }
+            }
+        }
 
         override fun createAnimator(
             sceenRoot: ViewGroup,
@@ -109,72 +150,58 @@
                 return null
             }
 
-            var fromVis = startValues.values[PROP_VISIBILITY] as Int
-            var fromIsVis = fromVis == View.VISIBLE
-            var fromAlpha = startValues.values[PROP_ALPHA] as Float
-            val fromBounds = startValues.values[PROP_BOUNDS] as Rect
-            val fromSSBounds = startValues.values[SMARTSPACE_BOUNDS] as Rect?
+            val from = Target.fromStart(startValues)
+            val to = Target.fromEnd(endValues)
+            initTargets(from, to)
+            mutateTargets(from, to)
 
-            val toView = endValues.view
-            val toVis = endValues.values[PROP_VISIBILITY] as Int
-            val toBounds = endValues.values[PROP_BOUNDS] as Rect
-            val toSSBounds = endValues.values[SMARTSPACE_BOUNDS] as Rect?
-            val toIsVis = toVis == View.VISIBLE
-            val toAlpha = if (toIsVis) 1f else 0f
-
-            // Align starting visibility and alpha
-            if (!fromIsVis) fromAlpha = 0f
-            else if (fromAlpha <= 0f) {
-                fromIsVis = false
-                fromVis = View.INVISIBLE
-            }
-
-            mutateBounds(toView, fromIsVis, toIsVis, fromBounds, toBounds, fromSSBounds, toSSBounds)
-            if (fromIsVis == toIsVis && fromBounds.equals(toBounds)) {
+            if (from.isVisible == to.isVisible && from.bounds.equals(to.bounds)) {
                 if (DEBUG) {
                     Log.w(
                         TAG,
-                        "Skipping no-op transition: $toView; " +
-                            "vis: $fromVis -> $toVis; " +
-                            "alpha: $fromAlpha -> $toAlpha; " +
-                            "bounds: $fromBounds -> $toBounds; ",
+                        "Skipping no-op transition: ${to.view}; " +
+                            "vis: ${from.visibility} -> ${to.visibility}; " +
+                            "alpha: ${from.alpha} -> ${to.alpha}; " +
+                            "bounds: ${from.bounds} -> ${to.bounds}; ",
                     )
                 }
                 return null
             }
 
-            val sendToBack = fromIsVis && !toIsVis
+            val sendToBack = from.isVisible && !to.isVisible
             fun lerp(start: Int, end: Int, fract: Float): Int =
                 MathUtils.lerp(start.toFloat(), end.toFloat(), fract).toInt()
             fun computeBounds(fract: Float): Rect =
                 Rect(
-                    lerp(fromBounds.left, toBounds.left, fract),
-                    lerp(fromBounds.top, toBounds.top, fract),
-                    lerp(fromBounds.right, toBounds.right, fract),
-                    lerp(fromBounds.bottom, toBounds.bottom, fract),
+                    lerp(from.bounds.left, to.bounds.left, fract),
+                    lerp(from.bounds.top, to.bounds.top, fract),
+                    lerp(from.bounds.right, to.bounds.right, fract),
+                    lerp(from.bounds.bottom, to.bounds.bottom, fract),
                 )
 
             fun assignAnimValues(src: String, fract: Float, vis: Int? = null) {
+                mutateTargets(from, to)
                 val bounds = computeBounds(fract)
-                val alpha = MathUtils.lerp(fromAlpha, toAlpha, fract)
+                val alpha = MathUtils.lerp(from.alpha, to.alpha, fract)
                 if (DEBUG) {
                     Log.i(
                         TAG,
-                        "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;",
+                        "$src: ${to.view}; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;",
                     )
                 }
-                toView.setVisibility(vis ?: View.VISIBLE)
-                toView.setAlpha(alpha)
-                toView.setRect(bounds)
+
+                to.view.setVisibility(vis ?: View.VISIBLE)
+                to.view.setAlpha(alpha)
+                to.view.setRect(bounds)
             }
 
             if (DEBUG) {
                 Log.i(
                     TAG,
-                    "transitioning: $toView; " +
-                        "vis: $fromVis -> $toVis; " +
-                        "alpha: $fromAlpha -> $toAlpha; " +
-                        "bounds: $fromBounds -> $toBounds; ",
+                    "transitioning: ${to.view}; " +
+                        "vis: ${from.visibility} -> ${to.visibility}; " +
+                        "alpha: ${from.alpha} -> ${to.alpha}; " +
+                        "bounds: ${from.bounds} -> ${to.bounds}; ",
                 )
             }
 
@@ -190,11 +217,11 @@
                 this@VisibilityBoundsTransition.addListener(
                     object : TransitionListenerAdapter() {
                         override fun onTransitionStart(t: Transition) {
-                            toView.viewTreeObserver.addOnPreDrawListener(predrawCallback)
+                            to.view.viewTreeObserver.addOnPreDrawListener(predrawCallback)
                         }
 
                         override fun onTransitionEnd(t: Transition) {
-                            toView.viewTreeObserver.removeOnPreDrawListener(predrawCallback)
+                            to.view.viewTreeObserver.removeOnPreDrawListener(predrawCallback)
                         }
                     }
                 )
@@ -202,17 +229,17 @@
                 val listener =
                     object : AnimatorListenerAdapter() {
                         override fun onAnimationStart(anim: Animator) {
-                            assignAnimValues("start", 0f, fromVis)
+                            assignAnimValues("start", 0f, from.visibility)
                         }
 
                         override fun onAnimationEnd(anim: Animator) {
-                            assignAnimValues("end", 1f, toVis)
-                            if (sendToBack) toView.translationZ = 0f
+                            assignAnimValues("end", 1f, to.visibility)
+                            if (sendToBack) to.view.translationZ = 0f
                         }
                     }
 
                 anim.addListener(listener)
-                assignAnimValues("init", 0f, fromVis)
+                assignAnimValues("init", 0f, from.visibility)
             }
         }
 
@@ -251,31 +278,23 @@
             }
         }
 
-        override fun mutateBounds(
-            view: View,
-            fromIsVis: Boolean,
-            toIsVis: Boolean,
-            fromBounds: Rect,
-            toBounds: Rect,
-            fromSSBounds: Rect?,
-            toSSBounds: Rect?,
-        ) {
+        override fun initTargets(from: Target, to: Target) {
             // Move normally if clock is not changing visibility
-            if (fromIsVis == toIsVis) return
+            if (from.isVisible == to.isVisible) return
 
-            fromBounds.set(toBounds)
+            from.bounds.set(to.bounds)
             if (isLargeClock) {
                 // Large clock shouldn't move; fromBounds already set
-            } else if (toSSBounds != null && fromSSBounds != null) {
+            } else if (to.ssBounds != null && from.ssBounds != null) {
                 // Instead of moving the small clock the full distance, we compute the distance
                 // smartspace will move. We then scale this to match the duration of this animation
                 // so that the small clock moves at the same speed as smartspace.
                 val ssTranslation =
-                    abs((toSSBounds.top - fromSSBounds.top) * smallClockMoveScale).toInt()
-                fromBounds.top = toBounds.top - ssTranslation
-                fromBounds.bottom = toBounds.bottom - ssTranslation
+                    abs((to.ssBounds!!.top - from.ssBounds!!.top) * smallClockMoveScale).toInt()
+                from.bounds.top = to.bounds.top - ssTranslation
+                from.bounds.bottom = to.bounds.bottom - ssTranslation
             } else {
-                Log.e(TAG, "mutateBounds: smallClock received no smartspace bounds")
+                Log.e(TAG, "initTargets: smallClock received no smartspace bounds")
             }
         }
     }
@@ -320,10 +339,9 @@
         }
     }
 
-    // TODO: Might need a mechanism to update this one while in-progress
     class SmartspaceMoveTransition(
         val config: IntraBlueprintTransition.Config,
-        viewModel: KeyguardClockViewModel,
+        val viewModel: KeyguardClockViewModel,
     ) : VisibilityBoundsTransition() {
         private val isLargeClock = viewModel.isLargeClockVisible.value
         override val captureSmartspace = false
@@ -340,23 +358,23 @@
             addTarget(R.id.status_view_media_container)
         }
 
-        override fun mutateBounds(
-            view: View,
-            fromIsVis: Boolean,
-            toIsVis: Boolean,
-            fromBounds: Rect,
-            toBounds: Rect,
-            fromSSBounds: Rect?,
-            toSSBounds: Rect?,
-        ) {
+        override fun initTargets(from: Target, to: Target) {
             // If view is changing visibility, hold it in place
-            if (fromIsVis == toIsVis) return
-            if (DEBUG) Log.i(TAG, "Holding position of ${view.id}")
+            if (from.isVisible == to.isVisible) return
+            if (DEBUG) Log.i(TAG, "Holding position of ${to.view.id}")
 
-            if (fromIsVis) {
-                toBounds.set(fromBounds)
+            if (from.isVisible) {
+                to.bounds.set(from.bounds)
             } else {
-                fromBounds.set(toBounds)
+                from.bounds.set(to.bounds)
+            }
+        }
+
+        override fun mutateTargets(from: Target, to: Target) {
+            if (to.view.id == sharedR.id.date_smartspace_view) {
+                to.isVisible = !viewModel.hasCustomWeatherDataDisplay.value
+                to.visibility = if (to.isVisible) View.VISIBLE else View.GONE
+                to.alpha = if (to.isVisible) 1f else 0f
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index e3cf411..adf9eb4 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -58,6 +58,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.IRemoteCallback;
 import android.os.Looper;
 import android.os.PatternMatcher;
 import android.os.Process;
@@ -146,7 +147,6 @@
     public static final String TAG_OPS = "OverviewProxyService";
     private static final long BACKOFF_MILLIS = 1000;
     private static final long DEFERRED_CALLBACK_MILLIS = 5000;
-
     // Max backoff caps at 5 mins
     private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
 
@@ -183,6 +183,10 @@
     private int mConnectionBackoffAttempts;
     private boolean mBound;
     private boolean mIsEnabled;
+    // This is set to false when the overview service is requested to be bound until it is notified
+    // that the previous service has been cleaned up in IOverviewProxy#onUnbind(). It is also set to
+    // true after a 1000ms timeout by mDeferredBindAfterTimedOutCleanup.
+    private boolean mIsPrevServiceCleanedUp = true;
 
     private boolean mIsSystemOrVisibleBgUser;
     private int mCurrentBoundedUserId = -1;
@@ -489,6 +493,12 @@
         retryConnectionWithBackoff();
     };
 
+    private final Runnable mDeferredBindAfterTimedOutCleanup = () -> {
+        Log.w(TAG_OPS, "Timed out waiting for previous service to clean up, binding to new one");
+        mIsPrevServiceCleanedUp = true;
+        maybeBindService();
+    };
+
     private final BroadcastReceiver mUserEventReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -859,6 +869,7 @@
                 mShadeViewControllerLazy.get().cancelInputFocusTransfer();
             });
         }
+        mIsPrevServiceCleanedUp = true;
         startConnectionToCurrentUser();
     }
 
@@ -889,6 +900,19 @@
         }
         mHandler.removeCallbacks(mConnectionRunnable);
 
+        maybeBindService();
+    }
+
+    private void maybeBindService() {
+        if (!mIsPrevServiceCleanedUp) {
+            Log.w(TAG_OPS, "Skipping connection to TouchInteractionService until previous"
+                    + " instance is cleaned up.");
+            if (!mHandler.hasCallbacks(mDeferredConnectionCallback)) {
+                mHandler.postDelayed(mDeferredBindAfterTimedOutCleanup, BACKOFF_MILLIS);
+            }
+            return;
+        }
+
         // Avoid creating TouchInteractionService because the System user in HSUM mode does not
         // interact with UI elements
         UserHandle currentUser = UserHandle.of(mUserTracker.getUserId());
@@ -907,6 +931,7 @@
             Log.e(TAG_OPS, "Unable to bind because of security error", e);
         }
         if (mBound) {
+            mIsPrevServiceCleanedUp = false;
             // Ensure that connection has been established even if it thinks it is bound
             mHandler.postDelayed(mDeferredConnectionCallback, DEFERRED_CALLBACK_MILLIS);
         } else {
@@ -960,6 +985,24 @@
             // Always unbind the service (ie. if called through onNullBinding or onBindingDied)
             mContext.unbindService(mOverviewServiceConnection);
             mBound = false;
+            if (mOverviewProxy != null) {
+                try {
+                    mOverviewProxy.onUnbind(new IRemoteCallback.Stub() {
+                        @Override
+                        public void sendResult(Bundle data) throws RemoteException {
+                            // Received Launcher reply, try to bind anew.
+                            mIsPrevServiceCleanedUp = true;
+                            if (mHandler.hasCallbacks(mDeferredBindAfterTimedOutCleanup)) {
+                                mHandler.removeCallbacks(mDeferredBindAfterTimedOutCleanup);
+                                maybeBindService();
+                            }
+                        }
+                    });
+                } catch (RemoteException e) {
+                    Log.w(TAG_OPS, "disconnectFromLauncherService failed to notify Launcher");
+                    mIsPrevServiceCleanedUp = true;
+                }
+            }
         }
 
         if (mOverviewProxy != null) {
@@ -1189,6 +1232,7 @@
         pw.print("  mInputFocusTransferStartMillis="); pw.println(mInputFocusTransferStartMillis);
         pw.print("  mActiveNavBarRegion="); pw.println(mActiveNavBarRegion);
         pw.print("  mNavBarMode="); pw.println(mNavBarMode);
+        pw.print("  mIsPrevServiceCleanedUp="); pw.println(mIsPrevServiceCleanedUp);
         mSysUiState.dump(pw, args);
     }