Basic workspace wireless charging animation

(No animation on ambient or lockscreen yet)

Test: manual (testing w/o wireless charger, set DEBUG_WIRELESS=true)
Bug: 67598445
Change-Id: Ifeef72805930187b8f78e72fd44bd7da25724e0c
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index f5af80a..ebb5f9f 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -35,6 +35,8 @@
     void animateCollapsePanels();
     void togglePanel();
 
+    void showChargingAnimation(int batteryLevel);
+
     /**
      * Notifies the status bar of a System UI visibility flag change.
      *
diff --git a/packages/SystemUI/res/layout/wireless_charging_layout.xml b/packages/SystemUI/res/layout/wireless_charging_layout.xml
new file mode 100644
index 0000000..113282b
--- /dev/null
+++ b/packages/SystemUI/res/layout/wireless_charging_layout.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2018, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <!-- Circle animation -->
+    <com.android.systemui.charging.WirelessChargingView
+        android:id="@+id/wireless_charging_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:elevation="4dp"/>
+
+    <!-- Text inside circle -->
+    <LinearLayout
+        android:id="@+id/wireless_charging_text_layout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/wireless_charging_percentage"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:textSize="32sp"
+            android:textColor="?attr/wallpaperTextColor"/>
+
+        <TextView
+            android:id="@+id/wireless_charging_secondary_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:textColor="?attr/wallpaperTextColor"/>
+    </LinearLayout>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index b749a18..e702e2a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -893,4 +893,12 @@
     <dimen name="fingerprint_dialog_icon_size">44dp</dimen>
     <dimen name="fingerprint_dialog_fp_icon_size">60dp</dimen>
     <dimen name="fingerprint_dialog_animation_translation_offset">350dp</dimen>
+
+    <!-- WirelessCharging Animation values -->
+    <!-- Starting text size of batteryLevel for wireless charging animation -->
+    <dimen name="config_batteryLevelTextSizeStart" format="float">5.0</dimen>
+    <!-- Ending text size of batteryLevel for wireless charging animation -->
+    <dimen name="config_batteryLevelTextSizeEnd" format="float">32.0</dimen>
+    <!-- Wireless Charging battery level text animation duration -->
+    <integer name="config_batteryLevelTextAnimationDuration">400</integer>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
new file mode 100644
index 0000000..348855b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.charging;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+
+/**
+ * A WirelessChargingAnimation is a view containing view + animation for wireless charging.
+ * @hide
+ */
+public class WirelessChargingAnimation {
+
+    public static final long DURATION = 1400;
+    private static final String TAG = "WirelessChargingView";
+    private static final boolean LOCAL_LOGV = false;
+
+    private final WirelessChargingView mCurrentWirelessChargingView;
+    private static WirelessChargingView mPreviousWirelessChargingView;
+
+    /**
+     * Constructs an empty WirelessChargingAnimation object.  If looper is null,
+     * Looper.myLooper() is used.  Must set
+     * {@link WirelessChargingAnimation#mCurrentWirelessChargingView}
+     * before calling {@link #show} - can be done through {@link #makeWirelessChargingAnimation}.
+     * @hide
+     */
+    public WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper, int
+            batteryLevel) {
+        mCurrentWirelessChargingView = new WirelessChargingView(context, looper,
+                batteryLevel);
+    }
+
+    /**
+     * Creates a wireless charging animation object populated with next view.
+     * @hide
+     */
+    public static WirelessChargingAnimation makeWirelessChargingAnimation(@NonNull Context context,
+            @Nullable Looper looper, int batteryLevel) {
+        return new WirelessChargingAnimation(context, looper, batteryLevel);
+    }
+
+    /**
+     * Show the view for the specified duration.
+     */
+    public void show() {
+        if (mCurrentWirelessChargingView == null ||
+                mCurrentWirelessChargingView.mNextView == null) {
+            throw new RuntimeException("setView must have been called");
+        }
+
+        if (mPreviousWirelessChargingView != null) {
+            mPreviousWirelessChargingView.hide(0);
+        }
+
+        mPreviousWirelessChargingView = mCurrentWirelessChargingView;
+        mCurrentWirelessChargingView.show();
+        mCurrentWirelessChargingView.hide(DURATION);
+    }
+
+    private static class WirelessChargingView {
+        private static final int SHOW = 0;
+        private static final int HIDE = 1;
+
+        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+        private final Handler mHandler;
+
+        private int mGravity;
+
+        private View mView;
+        private View mNextView;
+        private WindowManager mWM;
+
+        public WirelessChargingView(Context context, @Nullable Looper looper, int batteryLevel) {
+            mNextView = new WirelessChargingLayout(context, batteryLevel);
+            mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER;
+
+            final WindowManager.LayoutParams params = mParams;
+            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+            params.format = PixelFormat.TRANSLUCENT;
+
+            params.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
+            params.setTitle("Charging Animation");
+            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+                    | WindowManager.LayoutParams.FLAG_DIM_BEHIND;
+
+            params.dimAmount = .3f;
+
+            if (looper == null) {
+                // Use Looper.myLooper() if looper is not specified.
+                looper = Looper.myLooper();
+                if (looper == null) {
+                    throw new RuntimeException(
+                            "Can't display wireless animation on a thread that has not called "
+                                    + "Looper.prepare()");
+                }
+            }
+
+            mHandler = new Handler(looper, null) {
+                @Override
+                public void handleMessage(Message msg) {
+                    switch (msg.what) {
+                        case SHOW: {
+                            handleShow();
+                            break;
+                        }
+                        case HIDE: {
+                            handleHide();
+                            // Don't do this in handleHide() because it is also invoked by
+                            // handleShow()
+                            mNextView = null;
+                            break;
+                        }
+                    }
+                }
+            };
+        }
+
+        public void show() {
+            if (LOCAL_LOGV) Log.v(TAG, "SHOW: " + this);
+            mHandler.obtainMessage(SHOW).sendToTarget();
+        }
+
+        public void hide(long duration) {
+            if (LOCAL_LOGV) Log.v(TAG, "HIDE: " + this);
+            mHandler.sendMessageDelayed(Message.obtain(mHandler, HIDE), duration);
+        }
+
+        private void handleShow() {
+            if (LOCAL_LOGV) {
+                Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView="
+                        + mNextView);
+            }
+
+            if (mView != mNextView) {
+                // remove the old view if necessary
+                handleHide();
+                mView = mNextView;
+                Context context = mView.getContext().getApplicationContext();
+                String packageName = mView.getContext().getOpPackageName();
+                if (context == null) {
+                    context = mView.getContext();
+                }
+                mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+                // We can resolve the Gravity here by using the Locale for getting
+                // the layout direction
+                final Configuration config = mView.getContext().getResources().getConfiguration();
+                final int gravity = Gravity.getAbsoluteGravity(mGravity,
+                        config.getLayoutDirection());
+                mParams.gravity = gravity;
+                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
+                    mParams.horizontalWeight = 1.0f;
+                }
+                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
+                    mParams.verticalWeight = 1.0f;
+                }
+                mParams.packageName = packageName;
+                mParams.hideTimeoutMilliseconds = DURATION;
+
+                if (mView.getParent() != null) {
+                    if (LOCAL_LOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
+                    mWM.removeView(mView);
+                }
+                if (LOCAL_LOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
+
+                try {
+                    mWM.addView(mView, mParams);
+                } catch (WindowManager.BadTokenException e) {
+                    Slog.d(TAG, "Unable to add wireless charging view. " + e);
+                }
+            }
+        }
+
+        private void handleHide() {
+            if (LOCAL_LOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
+            if (mView != null) {
+                if (mView.getParent() != null) {
+                    if (LOCAL_LOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
+                    mWM.removeViewImmediate(mView);
+                }
+
+                mView = null;
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
new file mode 100644
index 0000000..c78ea56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.charging;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+
+import java.text.NumberFormat;
+
+/**
+ * @hide
+ */
+public class WirelessChargingLayout extends FrameLayout {
+    private final static int UNKNOWN_BATTERY_LEVEL = -1;
+
+    public WirelessChargingLayout(Context context) {
+        super(context);
+        init(context, null);
+    }
+
+    public WirelessChargingLayout(Context context, int batterylLevel) {
+        super(context);
+        init(context, null, batterylLevel);
+    }
+
+    public WirelessChargingLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs);
+    }
+
+    private void init(Context c, AttributeSet attrs) {
+        init(c, attrs, -1);
+    }
+
+    private void init(Context c, AttributeSet attrs, int batteryLevel) {
+        final int mBatteryLevel = batteryLevel;
+
+        inflate(c, R.layout.wireless_charging_layout, this);
+
+        // where the circle animation occurs:
+        final WirelessChargingView mChargingView = findViewById(R.id.wireless_charging_view);
+
+        // amount of battery:
+        final TextView mPercentage = findViewById(R.id.wireless_charging_percentage);
+
+        // (optional) time until full charge if available
+        final TextView mSecondaryText = findViewById(R.id.wireless_charging_secondary_text);
+
+        if (batteryLevel != UNKNOWN_BATTERY_LEVEL) {
+            mPercentage.setText(NumberFormat.getPercentInstance().format(mBatteryLevel / 100f));
+
+            ValueAnimator animator = ObjectAnimator.ofFloat(mPercentage, "textSize",
+                    getContext().getResources().getFloat(R.dimen.config_batteryLevelTextSizeStart),
+                    getContext().getResources().getFloat(R.dimen.config_batteryLevelTextSizeEnd));
+
+            animator.setDuration((long) getContext().getResources().getInteger(
+                    R.integer.config_batteryLevelTextAnimationDuration));
+            animator.start();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingView.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingView.java
new file mode 100644
index 0000000..f5edf52
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingView.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.charging;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+final class WirelessChargingView extends View {
+
+    private Interpolator mInterpolator;
+    private float mPathGone;
+    private float mInterpolatedPathGone;
+    private long mAnimationStartTime;
+    private long mStartSpinCircleAnimationTime;
+    private long mAnimationOffset = 500;
+    private long mTotalAnimationDuration = WirelessChargingAnimation.DURATION - mAnimationOffset;
+    private long mExpandingCircle = (long) (mTotalAnimationDuration * .9);
+    private long mSpinCircleAnimationTime = mTotalAnimationDuration - mExpandingCircle;
+
+    private boolean mFinishedAnimatingSpinningCircles = false;
+
+    private int mStartAngle = -90;
+    private int mNumSmallCircles = 20;
+    private int mSmallCircleRadius = 10;
+
+    private int mMainCircleStartRadius = 100;
+    private int mMainCircleEndRadius = 230;
+    private int mMainCircleCurrentRadius = mMainCircleStartRadius;
+
+    private int mCenterX;
+    private int mCenterY;
+
+    private Paint mPaint;
+    private Context mContext;
+
+    public WirelessChargingView(Context context) {
+        super(context);
+        init(context, null);
+    }
+
+    public WirelessChargingView(Context context, AttributeSet attr) {
+        super(context, attr);
+        init(context, attr);
+    }
+
+    public WirelessChargingView(Context context, AttributeSet attr, int styleAttr) {
+        super(context, attr, styleAttr);
+        init(context, attr);
+    }
+
+    public void init(Context context, AttributeSet attr) {
+        mContext = context;
+        setupPaint();
+        mInterpolator = new DecelerateInterpolator();
+    }
+
+    private void setupPaint() {
+        mPaint = new Paint();
+        mPaint.setColor(Utils.getColorAttr(mContext, R.attr.wallpaperTextColor));
+    }
+
+    @Override
+    protected void onDraw(final Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mAnimationStartTime == 0) {
+            mAnimationStartTime = System.currentTimeMillis();
+        }
+
+        updateDrawingParameters();
+        drawCircles(canvas);
+
+        if (!mFinishedAnimatingSpinningCircles) {
+            invalidate();
+        }
+    }
+
+    /**
+     * Draws a larger circle of radius {@link WirelessChargingView#mMainCircleEndRadius} composed of
+     * {@link WirelessChargingView#mNumSmallCircles} smaller circles
+     * @param canvas
+     */
+    private void drawCircles(Canvas canvas) {
+        mCenterX = canvas.getWidth() / 2;
+        mCenterY = canvas.getHeight() / 2;
+
+        // angleOffset makes small circles look like they're moving around the main circle
+        float angleOffset = mPathGone * 10;
+
+        // draws mNumSmallCircles to compose a larger, main circle
+        for (int circle = 0; circle < mNumSmallCircles; circle++) {
+            double angle = ((mStartAngle + angleOffset) * Math.PI / 180) + (circle * ((2 * Math.PI)
+                    / mNumSmallCircles));
+
+            int x = (int) (mCenterX + Math.cos(angle) * (mMainCircleCurrentRadius +
+                    mSmallCircleRadius));
+            int y = (int) (mCenterY + Math.sin(angle) * (mMainCircleCurrentRadius +
+                    mSmallCircleRadius));
+
+            canvas.drawCircle(x, y, mSmallCircleRadius, mPaint);
+        }
+
+        if (mMainCircleCurrentRadius >= mMainCircleEndRadius && !isSpinCircleAnimationStarted()) {
+            mStartSpinCircleAnimationTime = System.currentTimeMillis();
+        }
+
+        if (isSpinAnimationFinished()) {
+            mFinishedAnimatingSpinningCircles = true;
+        }
+    }
+
+    private boolean isSpinCircleAnimationStarted() {
+        return mStartSpinCircleAnimationTime != 0;
+    }
+
+    private boolean isSpinAnimationFinished() {
+        return isSpinCircleAnimationStarted() && System.currentTimeMillis() -
+                mStartSpinCircleAnimationTime > mSpinCircleAnimationTime;
+    }
+
+    private void updateDrawingParameters() {
+        mPathGone = getPathGone(System.currentTimeMillis());
+        mInterpolatedPathGone = mInterpolator.getInterpolation(mPathGone);
+
+        if (mPathGone < 1.0f) {
+            mMainCircleCurrentRadius = mMainCircleStartRadius + (int) (mInterpolatedPathGone *
+                    (mMainCircleEndRadius - mMainCircleStartRadius));
+        } else {
+            mMainCircleCurrentRadius = mMainCircleEndRadius;
+        }
+    }
+
+    /**
+     * @return decimal depicting how far along the creation of the larger circle (of circles) is
+     * For values < 1.0, the larger circle is being drawn
+     * For values > 1.0 the larger circle has been drawn and further animation can occur
+     */
+    private float getPathGone(long now) {
+        return (float) (now - mAnimationStartTime) / (mExpandingCircle);
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
index f06cda0..aa08562 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java
@@ -14,6 +14,10 @@
 
 package com.android.systemui.globalactions;
 
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.Dependency;
 import com.android.systemui.SysUiServiceProvider;
@@ -25,10 +29,6 @@
 import com.android.systemui.statusbar.policy.ExtensionController;
 import com.android.systemui.statusbar.policy.ExtensionController.Extension;
 
-import android.content.Context;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-
 public class GlobalActionsComponent extends SystemUI implements Callbacks, GlobalActionsManager {
 
     private GlobalActions mPlugin;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 00bc62e..79e9f7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -89,6 +89,7 @@
     private static final int MSG_FINGERPRINT_HELP              = 41 << MSG_SHIFT;
     private static final int MSG_FINGERPRINT_ERROR             = 42 << MSG_SHIFT;
     private static final int MSG_FINGERPRINT_HIDE              = 43 << MSG_SHIFT;
+    private static final int MSG_SHOW_CHARGING_ANIMATION       = 44 << MSG_SHIFT;
 
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -150,6 +151,8 @@
         default void handleShowGlobalActionsMenu() { }
         default void handleShowShutdownUi(boolean isReboot, String reason) { }
 
+        default void showChargingAnimation(int batteryLevel) {  }
+
         default void onRotationProposal(int rotation, boolean isValid) { }
 
         default void showFingerprintDialog(Bundle bundle, IFingerprintDialogReceiver receiver) { }
@@ -474,6 +477,13 @@
     }
 
     @Override
+    public void showChargingAnimation(int batteryLevel) {
+        mHandler.removeMessages(MSG_SHOW_CHARGING_ANIMATION);
+        mHandler.obtainMessage(MSG_SHOW_CHARGING_ANIMATION, batteryLevel, 0)
+                .sendToTarget();
+    }
+
+    @Override
     public void onProposedRotationChanged(int rotation, boolean isValid) {
         synchronized (mLock) {
             mHandler.removeMessages(MSG_ROTATION_PROPOSAL);
@@ -751,6 +761,12 @@
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).hideFingerprintDialog();
                     }
+                    break;
+                case MSG_SHOW_CHARGING_ANIMATION:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).showChargingAnimation(msg.arg1);
+                    }
+                    break;
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 3b783c2..d13ecae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -85,6 +85,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
@@ -138,6 +139,7 @@
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.ActivityStarterDelegate;
 import com.android.systemui.AutoReinflateContainer;
+import com.android.systemui.charging.WirelessChargingAnimation;
 import com.android.systemui.DemoMode;
 import com.android.systemui.Dependency;
 import com.android.systemui.EventLogTags;
@@ -1419,7 +1421,6 @@
         mQSPanel.clickTile(tile);
     }
 
-
     private void updateClearAll() {
         if (!mClearAllEnabled) {
             return;
@@ -2422,6 +2423,18 @@
                 mask, fullscreenStackBounds, dockedStackBounds, sbModeChanged, mStatusBarMode);
     }
 
+    @Override
+    public void showChargingAnimation(int batteryLevel) {
+        if (mDozing) {
+            // ambient
+        } else if (mKeyguardManager.isKeyguardLocked()) {
+            // lockscreen
+        } else {
+            WirelessChargingAnimation.makeWirelessChargingAnimation(mContext, null,
+                    batteryLevel).show();
+        }
+    }
+
     void touchAutoHide() {
         // update transient bar autohide
         if (mStatusBarMode == MODE_SEMI_TRANSPARENT || (mNavigationBar != null
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 2a153ec..a536270 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -19,15 +19,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
-
-import com.android.internal.app.IAppOpsService;
-import com.android.internal.app.IBatteryStats;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.server.EventLogTags;
-import com.android.server.LocalServices;
-import com.android.server.policy.WindowManagerPolicy;
-
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -54,6 +45,15 @@
 import android.util.Slog;
 import android.view.inputmethod.InputMethodManagerInternal;
 
+import com.android.internal.app.IAppOpsService;
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.policy.WindowManagerPolicy;
+
 /**
  * Sends broadcasts about important power state changes.
  * <p>
@@ -96,6 +96,7 @@
     private final ActivityManagerInternal mActivityManagerInternal;
     private final InputManagerInternal mInputManagerInternal;
     private final InputMethodManagerInternal mInputMethodManagerInternal;
+    private final StatusBarManagerInternal mStatusBarManagerInternal;
     private final TrustManager mTrustManager;
 
     private final NotifierHandler mHandler;
@@ -142,6 +143,7 @@
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
         mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
         mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class);
+        mStatusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class);
         mTrustManager = mContext.getSystemService(TrustManager.class);
 
         mHandler = new NotifierHandler(looper);
@@ -545,9 +547,19 @@
     }
 
     /**
-     * Called when wireless charging has started so as to provide user feedback.
+     * Called when profile screen lock timeout has expired.
      */
-    public void onWirelessChargingStarted() {
+    public void onProfileTimeout(@UserIdInt int userId) {
+        final Message msg = mHandler.obtainMessage(MSG_PROFILE_TIMED_OUT);
+        msg.setAsynchronous(true);
+        msg.arg1 = userId;
+        mHandler.sendMessage(msg);
+    }
+
+    /**
+     * Called when wireless charging has started so as to provide user feedback (sound and visual).
+     */
+    public void onWirelessChargingStarted(int batteryLevel) {
         if (DEBUG) {
             Slog.d(TAG, "onWirelessChargingStarted");
         }
@@ -555,16 +567,7 @@
         mSuspendBlocker.acquire();
         Message msg = mHandler.obtainMessage(MSG_WIRELESS_CHARGING_STARTED);
         msg.setAsynchronous(true);
-        mHandler.sendMessage(msg);
-    }
-
-    /**
-     * Called when profile screen lock timeout has expired.
-     */
-    public void onProfileTimeout(@UserIdInt int userId) {
-        final Message msg = mHandler.obtainMessage(MSG_PROFILE_TIMED_OUT);
-        msg.setAsynchronous(true);
-        msg.arg1 = userId;
+        msg.arg1 = batteryLevel;
         mHandler.sendMessage(msg);
     }
 
@@ -715,7 +718,11 @@
                 }
             }
         }
+    }
 
+    private void showWirelessChargingStarted(int batteryLevel) {
+        playWirelessChargingStartedSound();
+        mStatusBarManagerInternal.showChargingAnimation(batteryLevel);
         mSuspendBlocker.release();
     }
 
@@ -738,7 +745,7 @@
                     sendNextBroadcast();
                     break;
                 case MSG_WIRELESS_CHARGING_STARTED:
-                    playWirelessChargingStartedSound();
+                    showWirelessChargingStarted(msg.arg1);
                     break;
                 case MSG_SCREEN_BRIGHTNESS_BOOST_CHANGED:
                     sendBrightnessBoostChangedBroadcast();
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 7273f62..fbdedce 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -16,6 +16,11 @@
 
 package com.android.server.power;
 
+import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
+import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
+import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING;
+import static android.os.PowerManagerInternal.WAKEFULNESS_DREAMING;
+
 import android.annotation.IntDef;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
@@ -103,11 +108,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 
-import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
-import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
-import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING;
-import static android.os.PowerManagerInternal.WAKEFULNESS_DREAMING;
-
 /**
  * The power manager service is responsible for coordinating power management
  * functions on the device.
@@ -119,6 +119,9 @@
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_SPEW = DEBUG && true;
 
+    // if DEBUG_WIRELESS=true, plays wireless charging animation w/ sound on every plug + unplug
+    private static final boolean DEBUG_WIRELESS = false;
+
     // Message: Sent when a user activity timeout occurs to update the power state.
     private static final int MSG_USER_ACTIVITY_TIMEOUT = 1;
     // Message: Sent when the device enters or exits a dreaming or dozing state.
@@ -1794,8 +1797,8 @@
 
                 // Tell the notifier whether wireless charging has started so that
                 // it can provide feedback to the user.
-                if (dockedOnWirelessCharger) {
-                    mNotifier.onWirelessChargingStarted();
+                if (dockedOnWirelessCharger || DEBUG_WIRELESS) {
+                    mNotifier.onWirelessChargingStarted(mBatteryLevel);
                 }
             }
 
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 95006ff..3ab771b 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -37,6 +37,8 @@
     void dismissKeyboardShortcutsMenu();
     void toggleKeyboardShortcutsMenu(int deviceId);
 
+    void showChargingAnimation(int batteryLevel);
+
     /**
      * Show picture-in-picture menu.
      */
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index d67f2d7..adb368b 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -319,6 +319,16 @@
         }
 
         @Override
+        public void showChargingAnimation(int batteryLevel) {
+            if (mBar != null) {
+                try {
+                    mBar.showChargingAnimation(batteryLevel);
+                } catch (RemoteException ex){
+                }
+            }
+        }
+
+        @Override
         public void showPictureInPictureMenu() {
             if (mBar != null) {
                 try {