• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.settingslib.flags.Flags.newStatusBarIcons;
21 import static com.android.systemui.DejankUtils.whitelistIpcs;
22 
23 import static java.lang.annotation.RetentionPolicy.SOURCE;
24 
25 import android.animation.LayoutTransition;
26 import android.animation.ObjectAnimator;
27 import android.annotation.IntDef;
28 import android.annotation.IntRange;
29 import android.annotation.Nullable;
30 import android.annotation.SuppressLint;
31 import android.content.Context;
32 import android.content.res.Configuration;
33 import android.content.res.Resources;
34 import android.content.res.TypedArray;
35 import android.graphics.Rect;
36 import android.graphics.drawable.Drawable;
37 import android.os.UserHandle;
38 import android.provider.Settings;
39 import android.text.TextUtils;
40 import android.util.AttributeSet;
41 import android.util.TypedValue;
42 import android.view.Gravity;
43 import android.view.LayoutInflater;
44 import android.widget.ImageView;
45 import android.widget.LinearLayout;
46 import android.widget.TextView;
47 
48 import androidx.annotation.StyleRes;
49 import androidx.annotation.VisibleForTesting;
50 
51 import com.android.app.animation.Interpolators;
52 import com.android.systemui.DualToneHandler;
53 import com.android.systemui.battery.unified.BatteryColors;
54 import com.android.systemui.battery.unified.BatteryDrawableState;
55 import com.android.systemui.battery.unified.BatteryLayersDrawable;
56 import com.android.systemui.battery.unified.ColorProfile;
57 import com.android.systemui.plugins.DarkIconDispatcher;
58 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
59 import com.android.systemui.res.R;
60 import com.android.systemui.statusbar.policy.BatteryController;
61 
62 import java.io.PrintWriter;
63 import java.lang.annotation.Retention;
64 import java.text.NumberFormat;
65 import java.util.ArrayList;
66 
67 public class BatteryMeterView extends LinearLayout implements DarkReceiver {
68 
69     @Retention(SOURCE)
70     @IntDef({MODE_DEFAULT, MODE_ON, MODE_OFF, MODE_ESTIMATE})
71     public @interface BatteryPercentMode {}
72     public static final int MODE_DEFAULT = 0;
73     public static final int MODE_ON = 1;
74     public static final int MODE_OFF = 2;
75     public static final int MODE_ESTIMATE = 3;
76 
77     private final AccessorizedBatteryDrawable mDrawable;
78     private final ImageView mBatteryIconView;
79     private TextView mBatteryPercentView;
80 
81     private final @StyleRes int mPercentageStyleId;
82     private int mTextColor;
83     private int mLevel;
84     private int mShowPercentMode = MODE_DEFAULT;
85     private boolean mShowPercentAvailable;
86     private String mEstimateText = null;
87     private boolean mPluggedIn;
88     private boolean mPowerSaveEnabled;
89     private boolean mIsBatteryDefender;
90     private boolean mIsIncompatibleCharging;
91     private boolean mDisplayShieldEnabled;
92     // Error state where we know nothing about the current battery state
93     private boolean mBatteryStateUnknown;
94     // Lazily-loaded since this is expected to be a rare-if-ever state
95     private Drawable mUnknownStateDrawable;
96 
97     private DualToneHandler mDualToneHandler;
98     private boolean mIsStaticColor = false;
99 
100     private BatteryEstimateFetcher mBatteryEstimateFetcher;
101 
102     // for Flags.newStatusBarIcons. The unified battery icon can show percent inside
103     @Nullable private BatteryLayersDrawable mUnifiedBattery;
104     private BatteryColors mUnifiedBatteryColors = BatteryColors.LIGHT_THEME_COLORS;
105     private BatteryDrawableState mUnifiedBatteryState =
106             BatteryDrawableState.Companion.getDefaultInitialState();
107 
BatteryMeterView(Context context, AttributeSet attrs)108     public BatteryMeterView(Context context, AttributeSet attrs) {
109         this(context, attrs, 0);
110     }
111 
BatteryMeterView(Context context, AttributeSet attrs, int defStyle)112     public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) {
113         super(context, attrs, defStyle);
114 
115         setOrientation(LinearLayout.HORIZONTAL);
116         setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
117 
118         TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView,
119                 defStyle, 0);
120         final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
121                 context.getColor(com.android.settingslib.R.color.meter_background_color));
122         mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0);
123 
124         mDrawable = new AccessorizedBatteryDrawable(context, frameColor);
125         atts.recycle();
126 
127         mShowPercentAvailable = context.getResources().getBoolean(
128                 com.android.internal.R.bool.config_battery_percentage_setting_available);
129 
130         setupLayoutTransition();
131 
132         mBatteryIconView = new ImageView(context);
133         if (newStatusBarIcons()) {
134             mUnifiedBattery = BatteryLayersDrawable.Companion
135                     .newBatteryDrawable(context, mUnifiedBatteryState);
136             mBatteryIconView.setImageDrawable(mUnifiedBattery);
137 
138             final MarginLayoutParams mlp = new MarginLayoutParams(
139                     getResources().getDimensionPixelSize(
140                             R.dimen.status_bar_battery_unified_icon_width),
141                     getResources().getDimensionPixelSize(
142                             R.dimen.status_bar_battery_unified_icon_height));
143             addView(mBatteryIconView, mlp);
144         } else {
145             mBatteryIconView.setImageDrawable(mDrawable);
146             final MarginLayoutParams mlp = new MarginLayoutParams(
147                     getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_width),
148                     getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_height));
149             mlp.setMargins(0, 0, 0,
150                     getResources().getDimensionPixelOffset(R.dimen.battery_margin_bottom));
151             addView(mBatteryIconView, mlp);
152         }
153 
154         updateShowPercent();
155         mDualToneHandler = new DualToneHandler(context);
156         // Init to not dark at all.
157         onDarkChanged(new ArrayList<Rect>(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
158 
159         setClipChildren(false);
160         setClipToPadding(false);
161     }
162 
163 
setBatteryDrawableState(BatteryDrawableState newState)164     private void setBatteryDrawableState(BatteryDrawableState newState) {
165         if (!newStatusBarIcons()) return;
166 
167         mUnifiedBatteryState = newState;
168         mUnifiedBattery.setBatteryState(mUnifiedBatteryState);
169     }
170 
setupLayoutTransition()171     private void setupLayoutTransition() {
172         LayoutTransition transition = new LayoutTransition();
173         transition.setDuration(200);
174 
175         // Animates appearing/disappearing of the battery percentage text using fade-in/fade-out
176         // and disables all other animation types
177         ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
178         transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
179         transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
180 
181         ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
182         transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
183         transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
184 
185         transition.setAnimator(LayoutTransition.CHANGE_APPEARING, null);
186         transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, null);
187         transition.setAnimator(LayoutTransition.CHANGING, null);
188 
189         setLayoutTransition(transition);
190     }
191 
setForceShowPercent(boolean show)192     public void setForceShowPercent(boolean show) {
193         setPercentShowMode(show ? MODE_ON : MODE_DEFAULT);
194     }
195 
196     /**
197      * Force a particular mode of showing percent
198      *
199      * 0 - No preference
200      * 1 - Force on
201      * 2 - Force off
202      * 3 - Estimate
203      * @param mode desired mode (none, on, off)
204      */
setPercentShowMode(@atteryPercentMode int mode)205     public void setPercentShowMode(@BatteryPercentMode int mode) {
206         if (mode == mShowPercentMode) return;
207         mShowPercentMode = mode;
208         updateShowPercent();
209         updatePercentText();
210     }
211 
212     @Override
onConfigurationChanged(Configuration newConfig)213     protected void onConfigurationChanged(Configuration newConfig) {
214         super.onConfigurationChanged(newConfig);
215         updatePercentView();
216         mDrawable.notifyDensityChanged();
217     }
218 
setColorsFromContext(Context context)219     public void setColorsFromContext(Context context) {
220         if (context == null) {
221             return;
222         }
223 
224         mDualToneHandler.setColorsFromContext(context);
225     }
226 
227     @Override
hasOverlappingRendering()228     public boolean hasOverlappingRendering() {
229         return false;
230     }
231 
232     /**
233      * Update battery level
234      *
235      * @param level     int between 0 and 100 (representing percentage value)
236      * @param pluggedIn whether the device is plugged in or not
237      */
onBatteryLevelChanged(@ntRangefrom = 0, to = 100) int level, boolean pluggedIn)238     public void onBatteryLevelChanged(@IntRange(from = 0, to = 100) int level, boolean pluggedIn) {
239         boolean wasCharging = isCharging();
240         mPluggedIn = pluggedIn;
241         mLevel = level;
242         boolean isCharging = isCharging();
243         mDrawable.setCharging(isCharging);
244         mDrawable.setBatteryLevel(level);
245         updatePercentText();
246 
247         if (newStatusBarIcons()) {
248             Drawable attr = mUnifiedBatteryState.getAttribution();
249             if (isCharging != wasCharging) {
250                 attr = getBatteryAttribution(isCharging);
251             }
252 
253             BatteryDrawableState newState =
254                     new BatteryDrawableState(
255                             level,
256                             mUnifiedBatteryState.getShowPercent(),
257                             getCurrentColorProfile(),
258                             attr
259                     );
260 
261             setBatteryDrawableState(newState);
262         }
263     }
264 
265     // Potentially reloads any attribution. Should not be called if the state hasn't changed
266     @SuppressLint("UseCompatLoadingForDrawables")
getBatteryAttribution(boolean isCharging)267     private Drawable getBatteryAttribution(boolean isCharging) {
268         if (!newStatusBarIcons()) return null;
269 
270         int resId = 0;
271         if (mPowerSaveEnabled) {
272             resId = R.drawable.battery_unified_attr_powersave;
273         } else if (mIsBatteryDefender && mDisplayShieldEnabled) {
274             resId = R.drawable.battery_unified_attr_defend;
275         } else if (isCharging) {
276             resId = R.drawable.battery_unified_attr_charging;
277         }
278 
279         Drawable attr = null;
280         if (resId > 0) {
281             attr = mContext.getDrawable(resId);
282         }
283 
284         return attr;
285     }
286 
287     /** Calculate the appropriate color for the current state */
getCurrentColorProfile()288     private ColorProfile getCurrentColorProfile() {
289         return getColorProfile(
290                 mPowerSaveEnabled,
291                 mIsBatteryDefender && mDisplayShieldEnabled,
292                 mPluggedIn,
293                 mLevel <= 20);
294     }
295 
296     /** pure function to compute the correct color profile for our battery icon */
getColorProfile( boolean isPowerSave, boolean isBatteryDefender, boolean isCharging, boolean isLowBattery )297     private ColorProfile getColorProfile(
298             boolean isPowerSave,
299             boolean isBatteryDefender,
300             boolean isCharging,
301             boolean isLowBattery
302     ) {
303         if (isCharging)  return ColorProfile.Active;
304         if (isPowerSave) return ColorProfile.Warning;
305         if (isBatteryDefender) return ColorProfile.None;
306         if (isLowBattery) return ColorProfile.Error;
307 
308         return ColorProfile.None;
309     }
310 
onPowerSaveChanged(boolean isPowerSave)311     void onPowerSaveChanged(boolean isPowerSave) {
312         if (isPowerSave == mPowerSaveEnabled) {
313             return;
314         }
315         mPowerSaveEnabled = isPowerSave;
316         if (!newStatusBarIcons()) {
317             mDrawable.setPowerSaveEnabled(isPowerSave);
318         } else {
319             setBatteryDrawableState(
320                     new BatteryDrawableState(
321                             mUnifiedBatteryState.getLevel(),
322                             mUnifiedBatteryState.getShowPercent(),
323                             getCurrentColorProfile(),
324                             getBatteryAttribution(isCharging())
325                     )
326             );
327         }
328     }
329 
onIsBatteryDefenderChanged(boolean isBatteryDefender)330     void onIsBatteryDefenderChanged(boolean isBatteryDefender) {
331         boolean valueChanged = mIsBatteryDefender != isBatteryDefender;
332         mIsBatteryDefender = isBatteryDefender;
333 
334         if (!valueChanged) {
335             return;
336         }
337 
338         updateContentDescription();
339         if (!newStatusBarIcons()) {
340             // The battery drawable is a different size depending on whether it's currently
341             // overheated or not, so we need to re-scale the view when overheated changes.
342             scaleBatteryMeterViews();
343         } else {
344             setBatteryDrawableState(
345                     new BatteryDrawableState(
346                             mUnifiedBatteryState.getLevel(),
347                             mUnifiedBatteryState.getShowPercent(),
348                             getCurrentColorProfile(),
349                             getBatteryAttribution(isCharging())
350                     )
351             );
352         }
353     }
354 
onIsIncompatibleChargingChanged(boolean isIncompatibleCharging)355     void onIsIncompatibleChargingChanged(boolean isIncompatibleCharging) {
356         boolean valueChanged = mIsIncompatibleCharging != isIncompatibleCharging;
357         mIsIncompatibleCharging = isIncompatibleCharging;
358         if (valueChanged) {
359             if (newStatusBarIcons()) {
360                 setBatteryDrawableState(
361                         new BatteryDrawableState(
362                                 mUnifiedBatteryState.getLevel(),
363                                 mUnifiedBatteryState.getShowPercent(),
364                                 getCurrentColorProfile(),
365                                 getBatteryAttribution(isCharging())
366                         )
367                 );
368             } else {
369                 mDrawable.setCharging(isCharging());
370             }
371             updateContentDescription();
372         }
373     }
374 
inflatePercentView()375     private TextView inflatePercentView() {
376         return (TextView) LayoutInflater.from(getContext())
377                 .inflate(R.layout.battery_percentage_view, null);
378     }
379 
addPercentView(TextView inflatedPercentView)380     private void addPercentView(TextView inflatedPercentView) {
381         mBatteryPercentView = inflatedPercentView;
382 
383         if (mPercentageStyleId != 0) { // Only set if specified as attribute
384             mBatteryPercentView.setTextAppearance(mPercentageStyleId);
385         }
386         float fontHeight = mBatteryPercentView.getPaint().getFontMetricsInt(null);
387         mBatteryPercentView.setLineHeight(TypedValue.COMPLEX_UNIT_PX, fontHeight);
388         if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
389         addView(mBatteryPercentView, new LayoutParams(
390                 LayoutParams.WRAP_CONTENT,
391                 (int) Math.ceil(fontHeight)));
392     }
393 
394     /**
395      * Updates percent view by removing old one and reinflating if necessary
396      */
updatePercentView()397     public void updatePercentView() {
398         if (mBatteryPercentView != null) {
399             removeView(mBatteryPercentView);
400             mBatteryPercentView = null;
401         }
402         updateShowPercent();
403     }
404 
405     /**
406      * Sets the fetcher that should be used to get the estimated time remaining for the user's
407      * battery.
408      */
setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher)409     void setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher) {
410         mBatteryEstimateFetcher = fetcher;
411     }
412 
setDisplayShieldEnabled(boolean displayShieldEnabled)413     void setDisplayShieldEnabled(boolean displayShieldEnabled) {
414         mDisplayShieldEnabled = displayShieldEnabled;
415     }
416 
updatePercentText()417     void updatePercentText() {
418         if (!newStatusBarIcons()) {
419             updatePercentTextLegacy();
420             return;
421         }
422 
423         // The unified battery can show the percent inside, so we only need to handle
424         // the estimated time remaining case
425         if (mShowPercentMode == MODE_ESTIMATE
426                 && mBatteryEstimateFetcher != null
427                 && !isCharging()
428         ) {
429             mBatteryEstimateFetcher.fetchBatteryTimeRemainingEstimate(
430                     (String estimate) -> {
431                         if (mBatteryPercentView == null) {
432                             // Similar to the legacy behavior, inflate and add the view. We will
433                             // only use it for the estimate text
434                             addPercentView(inflatePercentView());
435                         }
436                         if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
437                             mEstimateText = estimate;
438                             mBatteryPercentView.setText(estimate);
439                             updateContentDescription();
440                         } else {
441                             mEstimateText = null;
442                             mBatteryPercentView.setText(null);
443                             updateContentDescription();
444                         }
445                     });
446         } else {
447             if (mBatteryPercentView != null) {
448                 mEstimateText = null;
449                 mBatteryPercentView.setText(null);
450             }
451             updateContentDescription();
452         }
453     }
454 
updatePercentTextLegacy()455     void updatePercentTextLegacy() {
456         if (mBatteryStateUnknown) {
457             return;
458         }
459 
460         if (mBatteryEstimateFetcher == null) {
461             setPercentTextAtCurrentLevel();
462             return;
463         }
464 
465         if (mBatteryPercentView != null) {
466             if (mShowPercentMode == MODE_ESTIMATE && !isCharging()) {
467                 mBatteryEstimateFetcher.fetchBatteryTimeRemainingEstimate(
468                         (String estimate) -> {
469                     if (mBatteryPercentView == null) {
470                         return;
471                     }
472                     if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
473                         mEstimateText = estimate;
474                         mBatteryPercentView.setText(estimate);
475                         updateContentDescription();
476                     } else {
477                         setPercentTextAtCurrentLevel();
478                     }
479                 });
480             } else {
481                 setPercentTextAtCurrentLevel();
482             }
483         } else {
484             updateContentDescription();
485         }
486     }
487 
setPercentTextAtCurrentLevel()488     private void setPercentTextAtCurrentLevel() {
489         if (mBatteryPercentView != null) {
490             mEstimateText = null;
491             String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
492             // Setting text actually triggers a layout pass (because the text view is set to
493             // wrap_content width and TextView always relayouts for this). Avoid needless
494             // relayout if the text didn't actually change.
495             if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
496                 mBatteryPercentView.setText(percentText);
497             }
498         }
499 
500         updateContentDescription();
501     }
502 
updateContentDescription()503     private void updateContentDescription() {
504         Context context = getContext();
505 
506         String contentDescription;
507         if (mBatteryStateUnknown) {
508             contentDescription = context.getString(R.string.accessibility_battery_unknown);
509         } else if (mShowPercentMode == MODE_ESTIMATE && !TextUtils.isEmpty(mEstimateText)) {
510             contentDescription = context.getString(
511                     mIsBatteryDefender
512                             ? R.string.accessibility_battery_level_charging_paused_with_estimate
513                             : R.string.accessibility_battery_level_with_estimate,
514                     mLevel,
515                     mEstimateText);
516         } else if (mIsBatteryDefender) {
517             contentDescription =
518                     context.getString(R.string.accessibility_battery_level_charging_paused, mLevel);
519         } else if (isCharging()) {
520             contentDescription =
521                     context.getString(R.string.accessibility_battery_level_charging, mLevel);
522         } else {
523             contentDescription = context.getString(R.string.accessibility_battery_level, mLevel);
524         }
525 
526         setContentDescription(contentDescription);
527     }
528 
updateShowPercent()529     void updateShowPercent() {
530         if (!newStatusBarIcons()) {
531             updateShowPercentLegacy();
532             return;
533         }
534 
535         if (!mShowPercentAvailable || mUnifiedBattery == null) return;
536 
537         boolean shouldShow = mShowPercentMode == MODE_ON || mShowPercentMode == MODE_ESTIMATE;
538         if (!mBatteryStateUnknown && !shouldShow && (mShowPercentMode != MODE_OFF)) {
539             // Slow case: fall back to the system setting
540             // TODO(b/140051051)
541             shouldShow = 0 != whitelistIpcs(() -> Settings.System
542                     .getIntForUser(getContext().getContentResolver(),
543                     SHOW_BATTERY_PERCENT, getContext().getResources().getBoolean(
544                     com.android.internal.R.bool.config_defaultBatteryPercentageSetting)
545                     ? 1 : 0, UserHandle.USER_CURRENT));
546         }
547 
548         setBatteryDrawableState(
549                 new BatteryDrawableState(
550                         mUnifiedBatteryState.getLevel(),
551                         shouldShow,
552                         mUnifiedBatteryState.getColor(),
553                         mUnifiedBatteryState.getAttribution()
554                 )
555         );
556 
557         // The legacy impl used the percent view for the estimate and the percent text. The modern
558         // version only uses it for estimate. It can be safely removed here
559         if (mShowPercentMode != MODE_ESTIMATE) {
560             removeView(mBatteryPercentView);
561             mBatteryPercentView = null;
562         }
563     }
564 
updateShowPercentLegacy()565     private void updateShowPercentLegacy() {
566         final boolean showing = mBatteryPercentView != null;
567         // TODO(b/140051051)
568         final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System
569                 .getIntForUser(getContext().getContentResolver(),
570                 SHOW_BATTERY_PERCENT, getContext().getResources().getBoolean(
571                 com.android.internal.R.bool.config_defaultBatteryPercentageSetting)
572                 ? 1 : 0, UserHandle.USER_CURRENT));
573         boolean shouldShow =
574                 (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF)
575                 || mShowPercentMode == MODE_ON
576                 || mShowPercentMode == MODE_ESTIMATE;
577         shouldShow = shouldShow && !mBatteryStateUnknown;
578 
579         if (shouldShow) {
580             if (!showing) {
581                 addPercentView(inflatePercentView());
582                 updatePercentText();
583             }
584         } else {
585             if (showing) {
586                 removeView(mBatteryPercentView);
587                 mBatteryPercentView = null;
588             }
589         }
590     }
591 
getUnknownStateDrawable()592     private Drawable getUnknownStateDrawable() {
593         if (mUnknownStateDrawable == null) {
594             mUnknownStateDrawable = mContext.getDrawable(R.drawable.ic_battery_unknown);
595             mUnknownStateDrawable.setTint(mTextColor);
596         }
597 
598         return mUnknownStateDrawable;
599     }
600 
onBatteryUnknownStateChanged(boolean isUnknown)601     void onBatteryUnknownStateChanged(boolean isUnknown) {
602         if (mBatteryStateUnknown == isUnknown) {
603             return;
604         }
605 
606         mBatteryStateUnknown = isUnknown;
607         updateContentDescription();
608 
609         if (mBatteryStateUnknown) {
610             mBatteryIconView.setImageDrawable(getUnknownStateDrawable());
611         } else {
612             mBatteryIconView.setImageDrawable(mDrawable);
613         }
614 
615         updateShowPercent();
616     }
617 
scaleBatteryMeterViews()618     void scaleBatteryMeterViews() {
619         if (!newStatusBarIcons()) {
620             scaleBatteryMeterViewsLegacy();
621             return;
622         }
623 
624         // For simplicity's sake, copy the general pattern in the legacy method and use the new
625         // resources, excluding what we don't need
626         Resources res = getContext().getResources();
627         TypedValue typedValue = new TypedValue();
628 
629         res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
630         float iconScaleFactor = typedValue.getFloat();
631 
632         float mainBatteryHeight =
633                 res.getDimensionPixelSize(
634                         R.dimen.status_bar_battery_unified_icon_height) * iconScaleFactor;
635         float mainBatteryWidth =
636                 res.getDimensionPixelSize(
637                         R.dimen.status_bar_battery_unified_icon_width) * iconScaleFactor;
638 
639         LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
640                 Math.round(mainBatteryWidth),
641                 Math.round(mainBatteryHeight));
642 
643         mBatteryIconView.setLayoutParams(scaledLayoutParams);
644         mBatteryIconView.invalidateDrawable(mUnifiedBattery);
645     }
646 
647     /**
648      * Looks up the scale factor for status bar icons and scales the battery view by that amount.
649      */
scaleBatteryMeterViewsLegacy()650     void scaleBatteryMeterViewsLegacy() {
651         Resources res = getContext().getResources();
652         TypedValue typedValue = new TypedValue();
653 
654         res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
655         float iconScaleFactor = typedValue.getFloat();
656 
657         float mainBatteryHeight =
658                 res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height) * iconScaleFactor;
659         float mainBatteryWidth =
660                 res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
661 
662         boolean displayShield = mDisplayShieldEnabled && mIsBatteryDefender;
663         float fullBatteryIconHeight =
664                 BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
665         float fullBatteryIconWidth =
666                 BatterySpecs.getFullBatteryWidth(mainBatteryWidth, displayShield);
667 
668         int marginTop;
669         if (displayShield) {
670             // If the shield is displayed, we need some extra marginTop so that the bottom of the
671             // main icon is still aligned with the bottom of all the other system icons.
672             int shieldHeightAddition = Math.round(fullBatteryIconHeight - mainBatteryHeight);
673             // However, the other system icons have some embedded bottom padding that the battery
674             // doesn't have, so we shouldn't move the battery icon down by the full amount.
675             // See b/258672854.
676             marginTop = shieldHeightAddition
677                     - res.getDimensionPixelSize(R.dimen.status_bar_battery_extra_vertical_spacing);
678         } else {
679             marginTop = 0;
680         }
681 
682         int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
683 
684         LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
685                 Math.round(fullBatteryIconWidth),
686                 Math.round(fullBatteryIconHeight));
687         scaledLayoutParams.setMargins(0, marginTop, 0, marginBottom);
688 
689         mDrawable.setDisplayShield(displayShield);
690         mBatteryIconView.setLayoutParams(scaledLayoutParams);
691         mBatteryIconView.invalidateDrawable(mDrawable);
692     }
693 
694     @Override
onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint)695     public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) {
696         if (mIsStaticColor) return;
697 
698         if (!newStatusBarIcons()) {
699             onDarkChangedLegacy(areas, darkIntensity, tint);
700             return;
701         }
702 
703         if (mUnifiedBattery == null) {
704             return;
705         }
706 
707         if (DarkIconDispatcher.isInAreas(areas, this)) {
708             if (darkIntensity < 0.5) {
709                 mUnifiedBatteryColors = BatteryColors.DARK_THEME_COLORS;
710             } else {
711                 mUnifiedBatteryColors = BatteryColors.LIGHT_THEME_COLORS;
712             }
713 
714             mUnifiedBattery.setColors(mUnifiedBatteryColors);
715         } else  {
716             // Same behavior as the legacy code when not isInArea
717             mUnifiedBatteryColors = BatteryColors.DARK_THEME_COLORS;
718             mUnifiedBattery.setColors(mUnifiedBatteryColors);
719         }
720     }
721 
onDarkChangedLegacy(ArrayList<Rect> areas, float darkIntensity, int tint)722     private void onDarkChangedLegacy(ArrayList<Rect> areas, float darkIntensity, int tint) {
723         float intensity = DarkIconDispatcher.isInAreas(areas, this) ? darkIntensity : 0;
724         int nonAdaptedSingleToneColor = mDualToneHandler.getSingleColor(intensity);
725         int nonAdaptedForegroundColor = mDualToneHandler.getFillColor(intensity);
726         int nonAdaptedBackgroundColor = mDualToneHandler.getBackgroundColor(intensity);
727 
728         updateColors(nonAdaptedForegroundColor, nonAdaptedBackgroundColor,
729                 nonAdaptedSingleToneColor);
730     }
731 
setStaticColor(boolean isStaticColor)732     public void setStaticColor(boolean isStaticColor) {
733         mIsStaticColor = isStaticColor;
734     }
735 
736     /**
737      * Sets icon and text colors. This will be overridden by {@code onDarkChanged} events,
738      * if registered.
739      *
740      * @param foregroundColor
741      * @param backgroundColor
742      * @param singleToneColor
743      */
updateColors(int foregroundColor, int backgroundColor, int singleToneColor)744     public void updateColors(int foregroundColor, int backgroundColor, int singleToneColor) {
745         mDrawable.setColors(foregroundColor, backgroundColor, singleToneColor);
746         mTextColor = singleToneColor;
747         if (mBatteryPercentView != null) {
748             mBatteryPercentView.setTextColor(singleToneColor);
749         }
750 
751         if (mUnknownStateDrawable != null) {
752             mUnknownStateDrawable.setTint(singleToneColor);
753         }
754     }
755 
756     /** For newStatusBarIcons(), we use a BatteryColors object to declare the theme */
setUnifiedBatteryColors(BatteryColors colors)757     public void setUnifiedBatteryColors(BatteryColors colors) {
758         if (!newStatusBarIcons()) return;
759 
760         mUnifiedBatteryColors = colors;
761         mUnifiedBattery.setColors(mUnifiedBatteryColors);
762     }
763 
764     @VisibleForTesting
isCharging()765     boolean isCharging() {
766         return mPluggedIn && !mIsIncompatibleCharging;
767     }
768 
dump(PrintWriter pw, String[] args)769     public void dump(PrintWriter pw, String[] args) {
770         String powerSave = mDrawable == null ? null : mDrawable.getPowerSaveEnabled() + "";
771         String displayShield = mDrawable == null ? null : mDrawable.getDisplayShield() + "";
772         String charging = mDrawable == null ? null : mDrawable.getCharging() + "";
773         CharSequence percent = mBatteryPercentView == null ? null : mBatteryPercentView.getText();
774         pw.println("  BatteryMeterView:");
775         pw.println("    mDrawable.getPowerSave: " + powerSave);
776         pw.println("    mDrawable.getDisplayShield: " + displayShield);
777         pw.println("    mDrawable.getCharging: " + charging);
778         pw.println("    mBatteryPercentView.getText(): " + percent);
779         pw.println("    mTextColor: #" + Integer.toHexString(mTextColor));
780         pw.println("    mBatteryStateUnknown: " + mBatteryStateUnknown);
781         pw.println("    mIsIncompatibleCharging: " + mIsIncompatibleCharging);
782         pw.println("    mPluggedIn: " + mPluggedIn);
783         pw.println("    mLevel: " + mLevel);
784         pw.println("    mMode: " + mShowPercentMode);
785         if (newStatusBarIcons()) {
786             pw.println("    mUnifiedBatteryState: " + mUnifiedBatteryState);
787         }
788     }
789 
790     @VisibleForTesting
getBatteryPercentViewText()791     CharSequence getBatteryPercentViewText() {
792         return mBatteryPercentView.getText();
793     }
794 
795     @VisibleForTesting
getBatteryPercentView()796     TextView getBatteryPercentView() {
797         return mBatteryPercentView;
798     }
799 
800     @VisibleForTesting
getUnifiedBatteryState()801     BatteryDrawableState getUnifiedBatteryState() {
802         return mUnifiedBatteryState;
803     }
804 
805     /** An interface that will fetch the estimated time remaining for the user's battery. */
806     public interface BatteryEstimateFetcher {
fetchBatteryTimeRemainingEstimate( BatteryController.EstimateFetchCompletion completion)807         void fetchBatteryTimeRemainingEstimate(
808                 BatteryController.EstimateFetchCompletion completion);
809     }
810 }
811 
812