Animate LockScreen and AOD clock text for charging events

Test: Manual
Bug: 182719493

Change-Id: I57f7cd834a08d8846a35cdf731611fc7a239b85a
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index e025e27..00c27bf 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -42,6 +42,7 @@
             android:typeface="monospace"
             android:elegantTextHeight="false"
             android:singleLine="true"
+            chargeAnimationDelay="350"
             dozeWeight="200"
             lockScreenWeight="400"
         />
@@ -62,6 +63,7 @@
             android:fontFamily="@font/clock"
             android:typeface="monospace"
             android:elegantTextHeight="false"
+            chargeAnimationDelay="200"
             dozeWeight="200"
             lockScreenWeight="400"
         />
diff --git a/packages/SystemUI/res-keyguard/values/attrs.xml b/packages/SystemUI/res-keyguard/values/attrs.xml
index eb7a1f7..25be37a 100644
--- a/packages/SystemUI/res-keyguard/values/attrs.xml
+++ b/packages/SystemUI/res-keyguard/values/attrs.xml
@@ -45,5 +45,6 @@
     <declare-styleable name="AnimatableClockView">
         <attr name="dozeWeight" format="integer" />
         <attr name="lockScreenWeight" format="integer" />
+        <attr name="chargeAnimationDelay" format="integer" />
     </declare-styleable>
 </resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
index ab219f3..60b677a 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
@@ -27,6 +27,7 @@
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.ViewController;
 
 import java.util.Locale;
@@ -45,6 +46,7 @@
     private int mLockScreenColor;
 
     private boolean mIsDozing;
+    private boolean mIsCharging;
     private float mDozeAmount;
     private Locale mLocale;
 
@@ -56,7 +58,8 @@
     public AnimatableClockController(
             AnimatableClockView view,
             StatusBarStateController statusBarStateController,
-            BroadcastDispatcher broadcastDispatcher) {
+            BroadcastDispatcher broadcastDispatcher,
+            BatteryController batteryController) {
         super(view);
         mStatusBarStateController = statusBarStateController;
         mIsDozing = mStatusBarStateController.isDozing();
@@ -68,6 +71,16 @@
                 R.dimen.keyguard_clock_line_spacing_scale_burmese);
         mDefaultLineSpacing = getContext().getResources().getFloat(
                 R.dimen.keyguard_clock_line_spacing_scale);
+
+        batteryController.addCallback(new BatteryController.BatteryStateChangeCallback() {
+            @Override
+            public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+                if (!mIsCharging && charging) {
+                    mView.animateCharge(mIsDozing);
+                }
+                mIsCharging = charging;
+            }
+        });
     }
 
     private BroadcastReceiver mLocaleBroadcastReceiver = new BroadcastReceiver() {
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java
index c918d98..0d6f64f 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java
@@ -42,7 +42,9 @@
     private static final CharSequence DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm";
     private static final CharSequence SINGLE_LINE_FORMAT_12_HOUR = "h:mm";
     private static final CharSequence SINGLE_LINE_FORMAT_24_HOUR = "H:mm";
-    private static final long ANIM_DURATION = 300;
+    private static final long DOZE_ANIM_DURATION = 300;
+    private static final long CHARGE_ANIM_DURATION_PHASE_0 = 500;
+    private static final long CHARGE_ANIM_DURATION_PHASE_1 = 1000;
 
     private final Calendar mTime = Calendar.getInstance();
 
@@ -53,6 +55,7 @@
     private int mDozingColor;
     private int mLockScreenColor;
     private float mLineSpacingScale = 1f;
+    private int mChargeAnimationDelay = 0;
 
     private TextAnimator mTextAnimator = null;
     private Runnable mOnTextAnimatorInitialized;
@@ -79,6 +82,8 @@
         try {
             mDozingWeight = ta.getInt(R.styleable.AnimatableClockView_dozeWeight, 100);
             mLockScreenWeight = ta.getInt(R.styleable.AnimatableClockView_lockScreenWeight, 300);
+            mChargeAnimationDelay = ta.getInt(
+                    R.styleable.AnimatableClockView_chargeAnimationDelay, 200);
         } finally {
             ta.recycle();
         }
@@ -150,11 +155,36 @@
         mLockScreenColor = lockScreenColor;
     }
 
+    void animateCharge(boolean isDozing) {
+        if (mTextAnimator == null || mTextAnimator.isRunning()) {
+            // Skip charge animation if dozing animation is already playing.
+            return;
+        }
+        Runnable startAnimPhase2 = () -> setTextStyle(
+                isDozing ? mDozingWeight : mLockScreenWeight/* weight */,
+                -1,
+                null,
+                true /* animate */,
+                CHARGE_ANIM_DURATION_PHASE_1,
+                0 /* delay */,
+                null /* onAnimationEnd */);
+        setTextStyle(isDozing ? mLockScreenWeight : mDozingWeight/* weight */,
+                -1,
+                null,
+                true /* animate */,
+                CHARGE_ANIM_DURATION_PHASE_0,
+                mChargeAnimationDelay,
+                startAnimPhase2);
+    }
+
     void animateDoze(boolean isDozing, boolean animate) {
         setTextStyle(isDozing ? mDozingWeight : mLockScreenWeight /* weight */,
                 -1,
                 isDozing ? mDozingColor : mLockScreenColor,
-                animate);
+                animate,
+                DOZE_ANIM_DURATION,
+                0 /* delay */,
+                null /* onAnimationEnd */);
     }
 
     /**
@@ -170,15 +200,20 @@
     private void setTextStyle(
             @IntRange(from = 0, to = 1000) int weight,
             @FloatRange(from = 0) float textSize,
-            int color,
-            boolean animate) {
+            Integer color,
+            boolean animate,
+            long duration,
+            long delay,
+            Runnable onAnimationEnd) {
         if (mTextAnimator != null) {
-            mTextAnimator.setTextStyle(weight, textSize, color, animate, ANIM_DURATION, null);
+            mTextAnimator.setTextStyle(weight, textSize, color, animate, duration, null,
+                    delay, onAnimationEnd);
         } else {
             // when the text animator is set, update its start values
             mOnTextAnimatorInitialized =
                     () -> mTextAnimator.setTextStyle(
-                            weight, textSize, color, false, ANIM_DURATION, null);
+                            weight, textSize, color, false, duration, null,
+                            delay, onAnimationEnd);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 874b4d9..032ed7d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -49,6 +49,7 @@
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.ViewController;
 
 import java.util.Locale;
@@ -70,6 +71,7 @@
     private final KeyguardSliceViewController mKeyguardSliceViewController;
     private final NotificationIconAreaController mNotificationIconAreaController;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final BatteryController mBatteryController;
 
     /**
      * Clock for both small and large sizes
@@ -118,7 +120,8 @@
             BroadcastDispatcher broadcastDispatcher,
             PluginManager pluginManager,
             FeatureFlags featureFlags,
-            @Main Executor uiExecutor) {
+            @Main Executor uiExecutor,
+            BatteryController batteryController) {
         super(keyguardClockSwitch);
         mResources = resources;
         mStatusBarStateController = statusBarStateController;
@@ -130,6 +133,7 @@
         mPluginManager = pluginManager;
         mIsSmartspaceEnabled = featureFlags.isSmartspaceEnabled();
         mUiExecutor = uiExecutor;
+        mBatteryController = batteryController;
     }
 
     /**
@@ -156,14 +160,16 @@
             new AnimatableClockController(
                 mView.findViewById(R.id.animatable_clock_view),
                 mStatusBarStateController,
-                mBroadcastDispatcher);
+                mBroadcastDispatcher,
+                mBatteryController);
         mClockViewController.init();
 
         mLargeClockViewController =
             new AnimatableClockController(
                 mView.findViewById(R.id.animatable_clock_view_large),
                 mStatusBarStateController,
-                mBroadcastDispatcher);
+                mBroadcastDispatcher,
+                mBatteryController);
         mLargeClockViewController.init();
 
         // If a smartspace plugin is detected, replace the existing smartspace
diff --git a/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt b/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt
index 5735a4f..cdb39ef 100644
--- a/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt
+++ b/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt
@@ -65,7 +65,9 @@
             invalidateCallback()
         }
         addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator?) = textInterpolator.rebase()
+            override fun onAnimationEnd(animation: Animator?) {
+                textInterpolator.rebase()
+            }
             override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
         })
     }
@@ -74,6 +76,10 @@
         textInterpolator.layout = layout
     }
 
+    fun isRunning(): Boolean {
+        return animator.isRunning
+    }
+
     fun draw(c: Canvas) = textInterpolator.draw(c)
 
     /**
@@ -101,7 +107,9 @@
         color: Int? = null,
         animate: Boolean = true,
         duration: Long = -1L,
-        interpolator: TimeInterpolator? = null
+        interpolator: TimeInterpolator? = null,
+        delay: Long = 0,
+        onAnimationEnd: Runnable? = null
     ) {
         if (animate) {
             animator.cancel()
@@ -120,12 +128,25 @@
         textInterpolator.onTargetPaintModified()
 
         if (animate) {
+            animator.startDelay = delay
             animator.duration = if (duration == -1L) {
                 DEFAULT_ANIMATION_DURATION
             } else {
                 duration
             }
             interpolator?.let { animator.interpolator = it }
+            if (onAnimationEnd != null) {
+                val listener = object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator?) {
+                        onAnimationEnd.run()
+                        animator.removeListener(this)
+                    }
+                    override fun onAnimationCancel(animation: Animator?) {
+                        animator.removeListener(this)
+                    }
+                }
+                animator.addListener(listener)
+            }
             animator.start()
         } else {
             // No animation is requested, thus set base and target state to the same state.
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 4e5502d..9017dd2 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -47,6 +47,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.statusbar.policy.BatteryController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -96,6 +97,8 @@
     private AnimatableClockView mClockView;
     @Mock
     private AnimatableClockView mLargeClockView;
+    @Mock
+    BatteryController mBatteryController;
 
     private KeyguardClockSwitchController mController;
 
@@ -127,7 +130,8 @@
                 mBroadcastDispatcher,
                 mPluginManager,
                 mFeatureFlags,
-                mExecutor);
+                mExecutor,
+                mBatteryController);
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
         when(mColorExtractor.getColors(anyInt())).thenReturn(mGradientColors);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt
index 7b4f14d..ad7f0cb 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.keyguard
 
+import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.testing.AndroidTestingRunner
 import android.text.Layout
@@ -25,7 +26,9 @@
 import com.android.systemui.SysuiTestCase
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.eq
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
@@ -107,4 +110,34 @@
         // Then, animation start should not be called.
         verify(valueAnimator, never()).start()
     }
+
+    @Test
+    fun testAnimationEnded() {
+        val layout = makeLayout("Hello, World", PAINT)
+        val valueAnimator = mock(ValueAnimator::class.java)
+        val textInterpolator = mock(TextInterpolator::class.java)
+        val paint = mock(TextPaint::class.java)
+        `when`(textInterpolator.targetPaint).thenReturn(paint)
+        val animationEndCallback = mock(Runnable::class.java)
+
+        val textAnimator = TextAnimator(layout, {}).apply {
+            this.textInterpolator = textInterpolator
+            this.animator = valueAnimator
+        }
+
+        textAnimator.setTextStyle(
+                weight = 400,
+                animate = true,
+                onAnimationEnd = animationEndCallback
+        )
+
+        // Verify animationEnd callback has been added.
+        val captor = ArgumentCaptor.forClass(AnimatorListenerAdapter::class.java)
+        verify(valueAnimator).addListener(captor.capture())
+        captor.value.onAnimationEnd(valueAnimator)
+
+        // Verify animationEnd callback has been invoked and removed.
+        verify(animationEndCallback).run()
+        verify(valueAnimator).removeListener(eq(captor.value))
+    }
 }