Merge "Fix 3-button showing up briefly on the AllSet page in setup" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 95dd24b..0b74e15 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -173,6 +173,13 @@
         }
     }
 
+    /** Notifies that the stash state is changing. */
+    public void onStashStateChanging() {
+        if (isAnimatingNewBubble()) {
+            mBubbleBarViewAnimator.onStashStateChangingWhileAnimating();
+        }
+    }
+
     //
     // The below animators are exposed to BubbleStashController so it can manage the stashing
     // animation.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index f689a05..4b3416c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -250,6 +250,11 @@
                 && !mBubblesShowingOnHome
                 && !mBubblesShowingOnOverview;
         if (mIsStashed != isStashed) {
+            // notify the view controller that the stash state is about to change so that it can
+            // cancel an ongoing animation if there is one.
+            // note that this has to be called before updating mIsStashed with the new value,
+            // otherwise interrupting an ongoing animation may update it again with the wrong state
+            mBarViewController.onStashStateChanging();
             mIsStashed = isStashed;
             if (mAnimator != null) {
                 mAnimator.cancel();
@@ -423,4 +428,29 @@
         mIsStashed = true;
         onIsStashedChanged();
     }
+
+    /**
+     * Updates the values of the internal animators after the new bubble animation was interrupted
+     *
+     * @param isStashed whether the current state should be stashed
+     * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
+     *                              bubble bar is showing to ensure that the stash animator runs
+     *                              smoothly.
+     */
+    public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) {
+        if (isStashed) {
+            mBubbleStashedHandleAlpha.setValue(1);
+            mIconAlphaForStash.setValue(0);
+            mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
+            mIconTranslationYForStash.updateValue(getStashTranslation());
+        } else {
+            mBubbleStashedHandleAlpha.setValue(0);
+            mHandleViewController.setTranslationYForSwipe(0);
+            mIconAlphaForStash.setValue(1);
+            mIconScaleForStash.updateValue(1);
+            mIconTranslationYForStash.updateValue(bubbleBarTranslationY);
+        }
+        mIsStashed = isStashed;
+        onIsStashedChanged();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index a6d0ff8..66521c1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -179,7 +179,17 @@
                 }
             }
         }
-        animator.addEndListener { _, _, _, _, _, _, _ ->
+        animator.addEndListener { _, _, _, canceled, _, _, _ ->
+            // if the show animation was canceled, also cancel the hide animation. this is typically
+            // canceled in this class, but could potentially be canceled elsewhere.
+            if (canceled) {
+                val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
+                scheduler.cancel(hideAnimation)
+                animatingBubble = null
+                bubbleBarView.onAnimatingBubbleCompleted()
+                bubbleBarView.relativePivotY = 1f
+                return@addEndListener
+            }
             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -200,6 +210,7 @@
      * 3. The third part is the overshoot. The handle is made fully visible.
      */
     private fun buildHideAnimation() = Runnable {
+        if (animatingBubble == null) return@Runnable
         val offset = bubbleStashController.diffBetweenHandleAndBarCenters
         val stashedHandleTranslationY =
             bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
@@ -238,9 +249,9 @@
                 }
             }
         }
-        animator.addEndListener { _, _, _, _, _, _, _ ->
+        animator.addEndListener { _, _, _, canceled, _, _, _ ->
             animatingBubble = null
-            bubbleStashController.stashBubbleBarImmediate()
+            if (!canceled) bubbleStashController.stashBubbleBarImmediate()
             bubbleBarView.onAnimatingBubbleCompleted()
             bubbleBarView.relativePivotY = 1f
             bubbleStashController.updateTaskbarTouchRegion()
@@ -256,4 +267,18 @@
         bubbleBarView.relativePivotY = 1f
         animatingBubble = null
     }
+
+    /** Notifies the animator that the taskbar area was touched during an animation. */
+    fun onStashStateChangingWhileAnimating() {
+        val hideAnimation = animatingBubble?.hideAnimation ?: return
+        scheduler.cancel(hideAnimation)
+        animatingBubble = null
+        bubbleStashController.stashedHandlePhysicsAnimator.cancel()
+        bubbleBarView.onAnimatingBubbleCompleted()
+        bubbleBarView.relativePivotY = 1f
+        bubbleStashController.onNewBubbleAnimationInterrupted(
+            /* isStashed= */ bubbleBarView.alpha == 0f,
+            bubbleBarView.translationY
+        )
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index f46fdac..7065075 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -43,6 +43,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -52,47 +53,23 @@
 class BubbleBarViewAnimatorTest {
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val animatorScheduler = TestBubbleBarViewAnimatorScheduler()
+    private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler
+    private lateinit var overflowView: BubbleView
+    private lateinit var bubbleView: BubbleView
+    private lateinit var bubble: BubbleBarBubble
+    private lateinit var bubbleBarView: BubbleBarView
+    private lateinit var bubbleStashController: BubbleStashController
 
     @Before
     fun setUp() {
+        animatorScheduler = TestBubbleBarViewAnimatorScheduler()
         PhysicsAnimatorTestUtils.prepareForTest()
     }
 
     @Test
     fun animateBubbleInForStashed() {
-        lateinit var overflowView: BubbleView
-        lateinit var bubbleView: BubbleView
-        lateinit var bubble: BubbleBarBubble
-        val bubbleBarView = BubbleBarView(context)
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
-            val inflater = LayoutInflater.from(context)
-
-            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
-            overflowView =
-                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
-            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
-            bubbleBarView.addView(overflowView)
-
-            val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
-            bubbleView =
-                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
-            bubble =
-                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
-            bubbleView.setBubble(bubble)
-            bubbleBarView.addView(bubbleView)
-        }
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-
-        val bubbleStashController = mock<BubbleStashController>()
-        whenever(bubbleStashController.isStashed).thenReturn(true)
-        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
-            .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
-        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
-            .thenReturn(HANDLE_TRANSLATION)
-        whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
-            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        setUpBubbleBar()
+        setUpBubbleStashController()
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
@@ -106,7 +83,7 @@
         }
 
         // let the animation start and wait for it to complete
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(handle.alpha).isEqualTo(0)
@@ -123,7 +100,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
         // let the animation start and wait for it to complete
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(handle.alpha).isEqualTo(1)
@@ -135,38 +112,8 @@
 
     @Test
     fun animateBubbleInForStashed_tapAnimatingBubble() {
-        lateinit var overflowView: BubbleView
-        lateinit var bubbleView: BubbleView
-        lateinit var bubble: BubbleBarBubble
-        val bubbleBarView = BubbleBarView(context)
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
-            val inflater = LayoutInflater.from(context)
-
-            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
-            overflowView =
-                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
-            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
-            bubbleBarView.addView(overflowView)
-
-            val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
-            bubbleView =
-                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
-            bubble =
-                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
-            bubbleView.setBubble(bubble)
-            bubbleBarView.addView(bubbleView)
-        }
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
-
-        val bubbleStashController = mock<BubbleStashController>()
-        whenever(bubbleStashController.isStashed).thenReturn(true)
-        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
-            .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
-        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
-            .thenReturn(HANDLE_TRANSLATION)
-        whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
-            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        setUpBubbleBar()
+        setUpBubbleStashController()
 
         val handle = View(context)
         val handleAnimator = PhysicsAnimator.getInstance(handle)
@@ -180,7 +127,7 @@
         }
 
         // let the animation start and wait for it to complete
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(handle.alpha).isEqualTo(0)
@@ -206,6 +153,151 @@
         assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
     }
 
+    @Test
+    fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        assertThat(handleAnimator.isRunning()).isTrue()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.onStashStateChangingWhileAnimating()
+        }
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
+
+        // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
+        // again
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        assertThat(handleAnimator.isRunning()).isFalse()
+    }
+
+    @Test
+    fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // execute the hide bubble animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        // wait for the hide animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        assertThat(handleAnimator.isRunning()).isTrue()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.onStashStateChangingWhileAnimating()
+        }
+
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        verify(bubbleStashController).onNewBubbleAnimationInterrupted(any(), any())
+
+        // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
+        // again
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        assertThat(handleAnimator.isRunning()).isFalse()
+    }
+
+    @Test
+    fun animateBubbleInForStashed_showAnimationCanceled() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        assertThat(handleAnimator.isRunning()).isTrue()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        handleAnimator.cancel()
+        assertThat(handleAnimator.isRunning()).isFalse()
+        assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+        assertThat(animatorScheduler.delayedBlock).isNull()
+    }
+
+    private fun setUpBubbleBar() {
+        bubbleBarView = BubbleBarView(context)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+            val inflater = LayoutInflater.from(context)
+
+            val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
+            overflowView =
+                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+            overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
+            bubbleBarView.addView(overflowView)
+
+            val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
+            bubbleView =
+                inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+            bubble =
+                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+            bubbleView.setBubble(bubble)
+            bubbleBarView.addView(bubbleView)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+    }
+
+    private fun setUpBubbleStashController() {
+        bubbleStashController = mock<BubbleStashController>()
+        whenever(bubbleStashController.isStashed).thenReturn(true)
+        whenever(bubbleStashController.diffBetweenHandleAndBarCenters)
+            .thenReturn(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS)
+        whenever(bubbleStashController.stashedHandleTranslationForNewBubbleAnimation)
+            .thenReturn(HANDLE_TRANSLATION)
+        whenever(bubbleStashController.bubbleBarTranslationYForTaskbar)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+    }
+
     private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
 
         var delayedBlock: Runnable? = null
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index a21d7be..16630967 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -44,7 +44,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.views.ActivityContext;
 
 /**
@@ -273,8 +272,8 @@
                             privateProfileManager.isPrivateSpaceItem(adapterItem);
                     if (icon.getAlpha() == 0 || icon.getAlpha() == 1) {
                         icon.setAlpha(isPrivateSpaceItem
-                                && privateProfileManager.getAnimationScrolling()
-                                && privateProfileManager.getAnimate()
+                                && (privateProfileManager.getAnimationScrolling() ||
+                                    privateProfileManager.getAnimate())
                                 && privateProfileManager.getCurrentState() == STATE_ENABLED
                                 ? 0 : 1);
                     }
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 551fa94..ae0e80c 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -89,7 +89,15 @@
  */
 public class PrivateProfileManager extends UserProfileManager {
     private static final int EXPAND_COLLAPSE_DURATION = 800;
-    private static final int SETTINGS_OPACITY_DURATION = 160;
+    private static final int SETTINGS_OPACITY_DURATION = 400;
+    private static final int TEXT_UNLOCK_OPACITY_DURATION = 300;
+    private static final int TEXT_LOCK_OPACITY_DURATION = 50;
+    private static final int APP_OPACITY_DURATION = 400;
+    private static final int APP_OPACITY_DELAY = 400;
+    private static final int SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY = 400;
+    private static final int SETTINGS_OPACITY_DELAY = 400;
+    private static final int LOCK_TEXT_OPACITY_DELAY = 500;
+    private static final int NO_DELAY = 0;
     private final ActivityAllAppsContainerView<?> mAllApps;
     private final Predicate<UserHandle> mPrivateProfileMatcher;
     private final int mPsHeaderHeight;
@@ -445,7 +453,6 @@
         if (getCurrentState() == STATE_ENABLED
                 && isPrivateSpaceSettingsAvailable()) {
             settingsButton.setVisibility(VISIBLE);
-            settingsButton.setAlpha(1f);
             settingsButton.setOnClickListener(
                     view -> {
                         logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP);
@@ -590,7 +597,9 @@
         List<BaseAllAppsAdapter.AdapterItem> allAppsAdapterItems =
                 mAllApps.getActiveRecyclerView().getApps().getAdapterItems();
         ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
-        alphaAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        alphaAnim.setDuration(APP_OPACITY_DURATION)
+                .setStartDelay(isExpanding ? APP_OPACITY_DELAY : NO_DELAY);
+        alphaAnim.setInterpolator(Interpolators.LINEAR);
         alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator valueAnimator) {
@@ -627,20 +636,25 @@
         }
         ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
         ViewGroup lockButton = mPSHeader.findViewById(R.id.ps_lock_unlock_button);
+        TextView lockText = lockButton.findViewById(R.id.lock_text);
         if (settingsAndLockGroup.getLayoutTransition() == null) {
             // Set a new transition if the current ViewGroup does not already contain one as each
             // transition should only happen once when applied.
             enableLayoutTransition(settingsAndLockGroup);
         }
+        settingsAndLockGroup.getLayoutTransition().setStartDelay(
+                LayoutTransition.CHANGING,
+                expand ? SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY : NO_DELAY);
         PropertySetter headerSetter = new AnimatedPropertySetter();
         ImageButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button);
         updateSettingsGearAlpha(settingsButton, expand, headerSetter);
+        updateLockTextAlpha(lockText, expand, headerSetter);
         AnimatorSet animatorSet = headerSetter.buildAnim();
         animatorSet.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
                 // Animate the collapsing of the text at the same time while updating lock button.
-                lockButton.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE);
+                lockText.setVisibility(expand ? VISIBLE : GONE);
                 setAnimationRunning(true);
             }
         });
@@ -696,6 +710,8 @@
         LayoutTransition settingsAndLockTransition = new LayoutTransition();
         settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING);
         settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION);
+        settingsAndLockTransition.setInterpolator(LayoutTransition.CHANGING,
+                Interpolators.STANDARD);
         settingsAndLockTransition.addTransitionListener(new LayoutTransition.TransitionListener() {
             @Override
             public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
@@ -716,7 +732,15 @@
             PropertySetter setter) {
         float toAlpha = expand ? 1 : 0;
         setter.setFloat(settingsButton, VIEW_ALPHA, toAlpha, Interpolators.LINEAR)
-                .setDuration(SETTINGS_OPACITY_DURATION).setStartDelay(0);
+                .setDuration(SETTINGS_OPACITY_DURATION).setStartDelay(expand ?
+                        SETTINGS_OPACITY_DELAY : NO_DELAY);
+    }
+
+    private void updateLockTextAlpha(TextView textView, boolean expand, PropertySetter setter) {
+        float toAlpha = expand ? 1 : 0;
+        setter.setFloat(textView, VIEW_ALPHA, toAlpha, Interpolators.LINEAR)
+                .setDuration(expand ? TEXT_UNLOCK_OPACITY_DURATION : TEXT_LOCK_OPACITY_DURATION)
+                .setStartDelay(expand ? LOCK_TEXT_OPACITY_DELAY : NO_DELAY);
     }
 
     void expandPrivateSpace() {