1 /*
2  * Copyright (C) 2021 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 package com.android.systemui.battery;
17 
18 import static android.provider.Settings.System.SHOW_BATTERY_PERCENT;
19 
20 import static com.android.systemui.DejankUtils.whitelistIpcs;
21 
22 import static java.lang.annotation.RetentionPolicy.SOURCE;
23 
24 import android.animation.LayoutTransition;
25 import android.animation.ObjectAnimator;
26 import android.annotation.IntDef;
27 import android.content.Context;
28 import android.content.res.Configuration;
29 import android.content.res.Resources;
30 import android.content.res.TypedArray;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.os.UserHandle;
34 import android.provider.Settings;
35 import android.util.AttributeSet;
36 import android.util.TypedValue;
37 import android.view.Gravity;
38 import android.view.LayoutInflater;
39 import android.view.ViewGroup;
40 import android.widget.ImageView;
41 import android.widget.LinearLayout;
42 import android.widget.TextView;
43 
44 import androidx.annotation.StyleRes;
45 import androidx.annotation.VisibleForTesting;
46 
47 import com.android.settingslib.graph.ThemedBatteryDrawable;
48 import com.android.systemui.DualToneHandler;
49 import com.android.systemui.R;
50 import com.android.systemui.animation.Interpolators;
51 import com.android.systemui.plugins.DarkIconDispatcher;
52 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
53 import com.android.systemui.statusbar.policy.BatteryController;
54 
55 import java.io.FileDescriptor;
56 import java.io.PrintWriter;
57 import java.lang.annotation.Retention;
58 import java.text.NumberFormat;
59 
60 public class BatteryMeterView extends LinearLayout implements DarkReceiver {
61 
62     @Retention(SOURCE)
63     @IntDef({MODE_DEFAULT, MODE_ON, MODE_OFF, MODE_ESTIMATE})
64     public @interface BatteryPercentMode {}
65     public static final int MODE_DEFAULT = 0;
66     public static final int MODE_ON = 1;
67     public static final int MODE_OFF = 2;
68     public static final int MODE_ESTIMATE = 3;
69 
70     private final ThemedBatteryDrawable mDrawable;
71     private final ImageView mBatteryIconView;
72     private TextView mBatteryPercentView;
73 
74     private final @StyleRes int mPercentageStyleId;
75     private int mTextColor;
76     private int mLevel;
77     private int mShowPercentMode = MODE_DEFAULT;
78     private boolean mShowPercentAvailable;
79     private boolean mCharging;
80     // Error state where we know nothing about the current battery state
81     private boolean mBatteryStateUnknown;
82     // Lazily-loaded since this is expected to be a rare-if-ever state
83     private Drawable mUnknownStateDrawable;
84 
85     private DualToneHandler mDualToneHandler;
86 
87     private int mNonAdaptedSingleToneColor;
88     private int mNonAdaptedForegroundColor;
89     private int mNonAdaptedBackgroundColor;
90 
91     private BatteryEstimateFetcher mBatteryEstimateFetcher;
92 
BatteryMeterView(Context context, AttributeSet attrs)93     public BatteryMeterView(Context context, AttributeSet attrs) {
94         this(context, attrs, 0);
95     }
96 
BatteryMeterView(Context context, AttributeSet attrs, int defStyle)97     public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) {
98         super(context, attrs, defStyle);
99 
100         setOrientation(LinearLayout.HORIZONTAL);
101         setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
102 
103         TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView,
104                 defStyle, 0);
105         final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
106                 context.getColor(R.color.meter_background_color));
107         mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0);
108         mDrawable = new ThemedBatteryDrawable(context, frameColor);
109         atts.recycle();
110 
111         mShowPercentAvailable = context.getResources().getBoolean(
112                 com.android.internal.R.bool.config_battery_percentage_setting_available);
113 
114         setupLayoutTransition();
115 
116         mBatteryIconView = new ImageView(context);
117         mBatteryIconView.setImageDrawable(mDrawable);
118         final MarginLayoutParams mlp = new MarginLayoutParams(
119                 getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_width),
120                 getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_height));
121         mlp.setMargins(0, 0, 0,
122                 getResources().getDimensionPixelOffset(R.dimen.battery_margin_bottom));
123         addView(mBatteryIconView, mlp);
124 
125         updateShowPercent();
126         mDualToneHandler = new DualToneHandler(context);
127         // Init to not dark at all.
128         onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
129 
130         setClipChildren(false);
131         setClipToPadding(false);
132     }
133 
setupLayoutTransition()134     private void setupLayoutTransition() {
135         LayoutTransition transition = new LayoutTransition();
136         transition.setDuration(200);
137 
138         ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
139         transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
140         transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
141 
142         ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
143         transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
144         transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
145 
146         setLayoutTransition(transition);
147     }
148 
setForceShowPercent(boolean show)149     public void setForceShowPercent(boolean show) {
150         setPercentShowMode(show ? MODE_ON : MODE_DEFAULT);
151     }
152 
153     /**
154      * Force a particular mode of showing percent
155      *
156      * 0 - No preference
157      * 1 - Force on
158      * 2 - Force off
159      * 3 - Estimate
160      * @param mode desired mode (none, on, off)
161      */
setPercentShowMode(@atteryPercentMode int mode)162     public void setPercentShowMode(@BatteryPercentMode int mode) {
163         if (mode == mShowPercentMode) return;
164         mShowPercentMode = mode;
165         updateShowPercent();
166     }
167 
168     @Override
onConfigurationChanged(Configuration newConfig)169     protected void onConfigurationChanged(Configuration newConfig) {
170         super.onConfigurationChanged(newConfig);
171         updatePercentView();
172     }
173 
setColorsFromContext(Context context)174     public void setColorsFromContext(Context context) {
175         if (context == null) {
176             return;
177         }
178 
179         mDualToneHandler.setColorsFromContext(context);
180     }
181 
182     @Override
hasOverlappingRendering()183     public boolean hasOverlappingRendering() {
184         return false;
185     }
186 
onBatteryLevelChanged(int level, boolean pluggedIn)187     void onBatteryLevelChanged(int level, boolean pluggedIn) {
188         mDrawable.setCharging(pluggedIn);
189         mDrawable.setBatteryLevel(level);
190         mCharging = pluggedIn;
191         mLevel = level;
192         updatePercentText();
193     }
194 
onPowerSaveChanged(boolean isPowerSave)195     void onPowerSaveChanged(boolean isPowerSave) {
196         mDrawable.setPowerSaveEnabled(isPowerSave);
197     }
198 
loadPercentView()199     private TextView loadPercentView() {
200         return (TextView) LayoutInflater.from(getContext())
201                 .inflate(R.layout.battery_percentage_view, null);
202     }
203 
204     /**
205      * Updates percent view by removing old one and reinflating if necessary
206      */
updatePercentView()207     public void updatePercentView() {
208         if (mBatteryPercentView != null) {
209             removeView(mBatteryPercentView);
210             mBatteryPercentView = null;
211         }
212         updateShowPercent();
213     }
214 
215     /**
216      * Sets the fetcher that should be used to get the estimated time remaining for the user's
217      * battery.
218      */
setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher)219     void setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher) {
220         mBatteryEstimateFetcher = fetcher;
221     }
222 
updatePercentText()223     void updatePercentText() {
224         if (mBatteryStateUnknown) {
225             setContentDescription(getContext().getString(R.string.accessibility_battery_unknown));
226             return;
227         }
228 
229         if (mBatteryEstimateFetcher == null) {
230             return;
231         }
232 
233         if (mBatteryPercentView != null) {
234             if (mShowPercentMode == MODE_ESTIMATE && !mCharging) {
235                 mBatteryEstimateFetcher.fetchBatteryTimeRemainingEstimate(
236                         (String estimate) -> {
237                     if (mBatteryPercentView == null) {
238                         return;
239                     }
240                     if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
241                         mBatteryPercentView.setText(estimate);
242                         setContentDescription(getContext().getString(
243                                 R.string.accessibility_battery_level_with_estimate,
244                                 mLevel, estimate));
245                     } else {
246                         setPercentTextAtCurrentLevel();
247                     }
248                 });
249             } else {
250                 setPercentTextAtCurrentLevel();
251             }
252         } else {
253             setContentDescription(
254                     getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
255                             : R.string.accessibility_battery_level, mLevel));
256         }
257     }
258 
setPercentTextAtCurrentLevel()259     private void setPercentTextAtCurrentLevel() {
260         if (mBatteryPercentView == null) {
261             return;
262         }
263         mBatteryPercentView.setText(
264                 NumberFormat.getPercentInstance().format(mLevel / 100f));
265         setContentDescription(
266                 getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
267                         : R.string.accessibility_battery_level, mLevel));
268     }
269 
updateShowPercent()270     void updateShowPercent() {
271         final boolean showing = mBatteryPercentView != null;
272         // TODO(b/140051051)
273         final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System
274                 .getIntForUser(getContext().getContentResolver(),
275                 SHOW_BATTERY_PERCENT, 0, UserHandle.USER_CURRENT));
276         boolean shouldShow =
277                 (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF)
278                 || mShowPercentMode == MODE_ON
279                 || mShowPercentMode == MODE_ESTIMATE;
280         shouldShow = shouldShow && !mBatteryStateUnknown;
281 
282         if (shouldShow) {
283             if (!showing) {
284                 mBatteryPercentView = loadPercentView();
285                 if (mPercentageStyleId != 0) { // Only set if specified as attribute
286                     mBatteryPercentView.setTextAppearance(mPercentageStyleId);
287                 }
288                 if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
289                 updatePercentText();
290                 addView(mBatteryPercentView,
291                         new ViewGroup.LayoutParams(
292                                 LayoutParams.WRAP_CONTENT,
293                                 LayoutParams.MATCH_PARENT));
294             }
295         } else {
296             if (showing) {
297                 removeView(mBatteryPercentView);
298                 mBatteryPercentView = null;
299             }
300         }
301     }
302 
getUnknownStateDrawable()303     private Drawable getUnknownStateDrawable() {
304         if (mUnknownStateDrawable == null) {
305             mUnknownStateDrawable = mContext.getDrawable(R.drawable.ic_battery_unknown);
306             mUnknownStateDrawable.setTint(mTextColor);
307         }
308 
309         return mUnknownStateDrawable;
310     }
311 
onBatteryUnknownStateChanged(boolean isUnknown)312     void onBatteryUnknownStateChanged(boolean isUnknown) {
313         if (mBatteryStateUnknown == isUnknown) {
314             return;
315         }
316 
317         mBatteryStateUnknown = isUnknown;
318 
319         if (mBatteryStateUnknown) {
320             mBatteryIconView.setImageDrawable(getUnknownStateDrawable());
321         } else {
322             mBatteryIconView.setImageDrawable(mDrawable);
323         }
324 
325         updateShowPercent();
326     }
327 
328     /**
329      * Looks up the scale factor for status bar icons and scales the battery view by that amount.
330      */
scaleBatteryMeterViews()331     void scaleBatteryMeterViews() {
332         Resources res = getContext().getResources();
333         TypedValue typedValue = new TypedValue();
334 
335         res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
336         float iconScaleFactor = typedValue.getFloat();
337 
338         int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height);
339         int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width);
340         int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
341 
342         LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
343                 (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
344         scaledLayoutParams.setMargins(0, 0, 0, marginBottom);
345 
346         mBatteryIconView.setLayoutParams(scaledLayoutParams);
347     }
348 
349     @Override
onDarkChanged(Rect area, float darkIntensity, int tint)350     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
351         float intensity = DarkIconDispatcher.isInArea(area, this) ? darkIntensity : 0;
352         mNonAdaptedSingleToneColor = mDualToneHandler.getSingleColor(intensity);
353         mNonAdaptedForegroundColor = mDualToneHandler.getFillColor(intensity);
354         mNonAdaptedBackgroundColor = mDualToneHandler.getBackgroundColor(intensity);
355 
356         updateColors(mNonAdaptedForegroundColor, mNonAdaptedBackgroundColor,
357                 mNonAdaptedSingleToneColor);
358     }
359 
360     /**
361      * Sets icon and text colors. This will be overridden by {@code onDarkChanged} events,
362      * if registered.
363      *
364      * @param foregroundColor
365      * @param backgroundColor
366      * @param singleToneColor
367      */
updateColors(int foregroundColor, int backgroundColor, int singleToneColor)368     public void updateColors(int foregroundColor, int backgroundColor, int singleToneColor) {
369         mDrawable.setColors(foregroundColor, backgroundColor, singleToneColor);
370         mTextColor = singleToneColor;
371         if (mBatteryPercentView != null) {
372             mBatteryPercentView.setTextColor(singleToneColor);
373         }
374 
375         if (mUnknownStateDrawable != null) {
376             mUnknownStateDrawable.setTint(singleToneColor);
377         }
378     }
379 
dump(FileDescriptor fd, PrintWriter pw, String[] args)380     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
381         String powerSave = mDrawable == null ? null : mDrawable.getPowerSaveEnabled() + "";
382         CharSequence percent = mBatteryPercentView == null ? null : mBatteryPercentView.getText();
383         pw.println("  BatteryMeterView:");
384         pw.println("    mDrawable.getPowerSave: " + powerSave);
385         pw.println("    mBatteryPercentView.getText(): " + percent);
386         pw.println("    mTextColor: #" + Integer.toHexString(mTextColor));
387         pw.println("    mBatteryStateUnknown: " + mBatteryStateUnknown);
388         pw.println("    mLevel: " + mLevel);
389         pw.println("    mMode: " + mShowPercentMode);
390     }
391 
392     @VisibleForTesting
getBatteryPercentViewText()393     CharSequence getBatteryPercentViewText() {
394         return mBatteryPercentView.getText();
395     }
396 
397     /** An interface that will fetch the estimated time remaining for the user's battery. */
398     public interface BatteryEstimateFetcher {
fetchBatteryTimeRemainingEstimate( BatteryController.EstimateFetchCompletion completion)399         void fetchBatteryTimeRemainingEstimate(
400                 BatteryController.EstimateFetchCompletion completion);
401     }
402 }
403 
404