1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.charging;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.graphics.PixelFormat;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.util.Log;
27 import android.util.Slog;
28 import android.view.Gravity;
29 import android.view.WindowManager;
30 
31 import com.android.internal.logging.UiEvent;
32 import com.android.internal.logging.UiEventLogger;
33 import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
34 
35 /**
36  * A WirelessChargingAnimation is a view containing view + animation for wireless charging.
37  * @hide
38  */
39 public class WirelessChargingAnimation {
40     public static final int UNKNOWN_BATTERY_LEVEL = -1;
41     public static final long DURATION = 1500;
42     private static final String TAG = "WirelessChargingView";
43     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
44 
45     private final WirelessChargingView mCurrentWirelessChargingView;
46     private static WirelessChargingView mPreviousWirelessChargingView;
47 
48     public interface Callback {
onAnimationStarting()49         void onAnimationStarting();
onAnimationEnded()50         void onAnimationEnded();
51     }
52 
53     /**
54      * Constructs an empty WirelessChargingAnimation object.  If looper is null,
55      * Looper.myLooper() is used.  Must set
56      * {@link WirelessChargingAnimation#mCurrentWirelessChargingView}
57      * before calling {@link #show} - can be done through {@link #makeWirelessChargingAnimation}.
58      * @hide
59      */
WirelessChargingAnimation(@onNull Context context, @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger)60     private WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper,
61             int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing,
62             RippleShape rippleShape, UiEventLogger uiEventLogger) {
63         mCurrentWirelessChargingView = new WirelessChargingView(context, looper,
64                 transmittingBatteryLevel, batteryLevel, callback, isDozing,
65                 rippleShape, uiEventLogger);
66     }
67 
68     /**
69      * Creates a wireless charging animation object populated with next view.
70      *
71      * @hide
72      */
makeWirelessChargingAnimation(@onNull Context context, @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger)73     public static WirelessChargingAnimation makeWirelessChargingAnimation(@NonNull Context context,
74             @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel,
75             Callback callback, boolean isDozing, RippleShape rippleShape,
76             UiEventLogger uiEventLogger) {
77         return new WirelessChargingAnimation(context, looper, transmittingBatteryLevel,
78                 batteryLevel, callback, isDozing, rippleShape, uiEventLogger);
79     }
80 
81     /**
82      * Creates a charging animation object using mostly default values for non-dozing and unknown
83      * battery level without charging number shown.
84      */
makeChargingAnimationWithNoBatteryLevel( @onNull Context context, RippleShape rippleShape, UiEventLogger uiEventLogger)85     public static WirelessChargingAnimation makeChargingAnimationWithNoBatteryLevel(
86             @NonNull Context context, RippleShape rippleShape, UiEventLogger uiEventLogger) {
87         return makeWirelessChargingAnimation(context, null,
88                 UNKNOWN_BATTERY_LEVEL, UNKNOWN_BATTERY_LEVEL, null, false,
89                 rippleShape, uiEventLogger);
90     }
91 
92     /**
93      * Show the view for the specified duration.
94      */
show(long delay)95     public void show(long delay) {
96         if (mCurrentWirelessChargingView == null ||
97                 mCurrentWirelessChargingView.mNextView == null) {
98             throw new RuntimeException("setView must have been called");
99         }
100 
101         if (mPreviousWirelessChargingView != null) {
102             mPreviousWirelessChargingView.hide(0);
103         }
104 
105         mPreviousWirelessChargingView = mCurrentWirelessChargingView;
106         mCurrentWirelessChargingView.show(delay);
107         mCurrentWirelessChargingView.hide(delay + DURATION);
108     }
109 
110     private static class WirelessChargingView {
111         private static final int SHOW = 0;
112         private static final int HIDE = 1;
113 
114         private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
115         private final Handler mHandler;
116         private final UiEventLogger mUiEventLogger;
117 
118         private int mGravity;
119         private WirelessChargingLayout mView;
120         private WirelessChargingLayout mNextView;
121         private WindowManager mWM;
122         private Callback mCallback;
123 
WirelessChargingView(Context context, @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger)124         public WirelessChargingView(Context context, @Nullable Looper looper,
125                 int transmittingBatteryLevel, int batteryLevel, Callback callback,
126                 boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger) {
127             mCallback = callback;
128             mNextView = new WirelessChargingLayout(context, transmittingBatteryLevel, batteryLevel,
129                     isDozing, rippleShape);
130             mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER;
131             mUiEventLogger = uiEventLogger;
132 
133             final WindowManager.LayoutParams params = mParams;
134             params.height = WindowManager.LayoutParams.MATCH_PARENT;
135             params.width = WindowManager.LayoutParams.MATCH_PARENT;
136             params.format = PixelFormat.TRANSLUCENT;
137             params.type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
138             params.setTitle("Charging Animation");
139             params.layoutInDisplayCutoutMode =
140                     WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
141             params.setFitInsetsTypes(0 /* ignore all system bar insets */);
142             params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
143                     | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
144             params.setTrustedOverlay();
145 
146             if (looper == null) {
147                 // Use Looper.myLooper() if looper is not specified.
148                 looper = Looper.myLooper();
149                 if (looper == null) {
150                     throw new RuntimeException(
151                             "Can't display wireless animation on a thread that has not called "
152                                     + "Looper.prepare()");
153                 }
154             }
155 
156             mHandler = new Handler(looper, null) {
157                 @Override
158                 public void handleMessage(Message msg) {
159                     switch (msg.what) {
160                         case SHOW: {
161                             handleShow();
162                             break;
163                         }
164                         case HIDE: {
165                             handleHide();
166                             // Don't do this in handleHide() because it is also invoked by
167                             // handleShow()
168                             mNextView = null;
169                             break;
170                         }
171                     }
172                 }
173             };
174         }
175 
show(long delay)176         public void show(long delay) {
177             if (DEBUG) Slog.d(TAG, "SHOW: " + this);
178             mHandler.sendMessageDelayed(Message.obtain(mHandler, SHOW), delay);
179         }
180 
hide(long duration)181         public void hide(long duration) {
182             mHandler.removeMessages(HIDE);
183 
184             if (DEBUG) Slog.d(TAG, "HIDE: " + this);
185             mHandler.sendMessageDelayed(Message.obtain(mHandler, HIDE), duration);
186         }
187 
handleShow()188         private void handleShow() {
189             if (DEBUG) {
190                 Slog.d(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView="
191                         + mNextView);
192             }
193 
194             if (mView != mNextView) {
195                 // remove the old view if necessary
196                 handleHide();
197                 mView = mNextView;
198                 Context context = mView.getContext().getApplicationContext();
199                 String packageName = mView.getContext().getOpPackageName();
200                 if (context == null) {
201                     context = mView.getContext();
202                 }
203                 mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
204                 mParams.packageName = packageName;
205                 mParams.hideTimeoutMilliseconds = DURATION;
206 
207                 if (mView.getParent() != null) {
208                     if (DEBUG) Slog.d(TAG, "REMOVE! " + mView + " in " + this);
209                     mWM.removeView(mView);
210                 }
211                 if (DEBUG) Slog.d(TAG, "ADD! " + mView + " in " + this);
212 
213                 try {
214                     if (mCallback != null) {
215                         mCallback.onAnimationStarting();
216                     }
217                     mWM.addView(mView, mParams);
218                     mUiEventLogger.log(WirelessChargingRippleEvent.WIRELESS_RIPPLE_PLAYED);
219                 } catch (WindowManager.BadTokenException e) {
220                     Slog.d(TAG, "Unable to add wireless charging view. " + e);
221                 }
222             }
223         }
224 
handleHide()225         private void handleHide() {
226             if (DEBUG) Slog.d(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
227             if (mView != null) {
228                 if (mView.getParent() != null) {
229                     if (DEBUG) Slog.d(TAG, "REMOVE! " + mView + " in " + this);
230                     if (mCallback != null) {
231                         mCallback.onAnimationEnded();
232                     }
233                     mWM.removeViewImmediate(mView);
234                 }
235 
236                 mView = null;
237             }
238         }
239 
240         enum WirelessChargingRippleEvent implements UiEventLogger.UiEventEnum {
241             @UiEvent(doc = "Wireless charging ripple effect played")
242             WIRELESS_RIPPLE_PLAYED(830);
243 
244             private final int mInt;
WirelessChargingRippleEvent(int id)245             WirelessChargingRippleEvent(int id) {
246                 mInt = id;
247             }
248 
getId()249             @Override public int getId() {
250                 return mInt;
251             }
252         }
253     }
254 }
255