• 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.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.annotation.IntRange;
28 import android.content.Context;
29 import android.content.res.Configuration;
30 import android.content.res.Resources;
31 import android.content.res.TypedArray;
32 import android.graphics.Rect;
33 import android.graphics.drawable.Drawable;
34 import android.os.UserHandle;
35 import android.provider.Settings;
36 import android.text.TextUtils;
37 import android.util.AttributeSet;
38 import android.util.TypedValue;
39 import android.view.Gravity;
40 import android.view.LayoutInflater;
41 import android.widget.ImageView;
42 import android.widget.LinearLayout;
43 import android.widget.TextView;
44 
45 import androidx.annotation.StyleRes;
46 import androidx.annotation.VisibleForTesting;
47 
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.PrintWriter;
56 import java.lang.annotation.Retention;
57 import java.text.NumberFormat;
58 import java.util.ArrayList;
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 AccessorizedBatteryDrawable 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 String mEstimateText = null;
80     private boolean mCharging;
81     private boolean mIsOverheated;
82     private boolean mDisplayShieldEnabled;
83     // Error state where we know nothing about the current battery state
84     private boolean mBatteryStateUnknown;
85     // Lazily-loaded since this is expected to be a rare-if-ever state
86     private Drawable mUnknownStateDrawable;
87 
88     private DualToneHandler mDualToneHandler;
89 
90     private int mNonAdaptedSingleToneColor;
91     private int mNonAdaptedForegroundColor;
92     private int mNonAdaptedBackgroundColor;
93 
94     private BatteryEstimateFetcher mBatteryEstimateFetcher;
95 
BatteryMeterView(Context context, AttributeSet attrs)96     public BatteryMeterView(Context context, AttributeSet attrs) {
97         this(context, attrs, 0);
98     }
99 
BatteryMeterView(Context context, AttributeSet attrs, int defStyle)100     public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) {
101         super(context, attrs, defStyle);
102 
103         setOrientation(LinearLayout.HORIZONTAL);
104         setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
105 
106         TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView,
107                 defStyle, 0);
108         final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
109                 context.getColor(R.color.meter_background_color));
110         mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0);
111         mDrawable = new AccessorizedBatteryDrawable(context, frameColor);
112         atts.recycle();
113 
114         mShowPercentAvailable = context.getResources().getBoolean(
115                 com.android.internal.R.bool.config_battery_percentage_setting_available);
116 
117         setupLayoutTransition();
118 
119         mBatteryIconView = new ImageView(context);
120         mBatteryIconView.setImageDrawable(mDrawable);
121         final MarginLayoutParams mlp = new MarginLayoutParams(
122                 getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_width),
123                 getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_height));
124         mlp.setMargins(0, 0, 0,
125                 getResources().getDimensionPixelOffset(R.dimen.battery_margin_bottom));
126         addView(mBatteryIconView, mlp);
127 
128         updateShowPercent();
129         mDualToneHandler = new DualToneHandler(context);
130         // Init to not dark at all.
131         onDarkChanged(new ArrayList<Rect>(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
132 
133         setClipChildren(false);
134         setClipToPadding(false);
135     }
136 
setupLayoutTransition()137     private void setupLayoutTransition() {
138         LayoutTransition transition = new LayoutTransition();
139         transition.setDuration(200);
140 
141         // Animates appearing/disappearing of the battery percentage text using fade-in/fade-out
142         // and disables all other animation types
143         ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
144         transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
145         transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
146 
147         ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
148         transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
149         transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
150 
151         transition.setAnimator(LayoutTransition.CHANGE_APPEARING, null);
152         transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, null);
153         transition.setAnimator(LayoutTransition.CHANGING, null);
154 
155         setLayoutTransition(transition);
156     }
157 
setForceShowPercent(boolean show)158     public void setForceShowPercent(boolean show) {
159         setPercentShowMode(show ? MODE_ON : MODE_DEFAULT);
160     }
161 
162     /**
163      * Force a particular mode of showing percent
164      *
165      * 0 - No preference
166      * 1 - Force on
167      * 2 - Force off
168      * 3 - Estimate
169      * @param mode desired mode (none, on, off)
170      */
setPercentShowMode(@atteryPercentMode int mode)171     public void setPercentShowMode(@BatteryPercentMode int mode) {
172         if (mode == mShowPercentMode) return;
173         mShowPercentMode = mode;
174         updateShowPercent();
175         updatePercentText();
176     }
177 
178     @Override
onConfigurationChanged(Configuration newConfig)179     protected void onConfigurationChanged(Configuration newConfig) {
180         super.onConfigurationChanged(newConfig);
181         updatePercentView();
182         mDrawable.notifyDensityChanged();
183     }
184 
setColorsFromContext(Context context)185     public void setColorsFromContext(Context context) {
186         if (context == null) {
187             return;
188         }
189 
190         mDualToneHandler.setColorsFromContext(context);
191     }
192 
193     @Override
hasOverlappingRendering()194     public boolean hasOverlappingRendering() {
195         return false;
196     }
197 
198     /**
199      * Update battery level
200      *
201      * @param level     int between 0 and 100 (representing percentage value)
202      * @param pluggedIn whether the device is plugged in or not
203      */
onBatteryLevelChanged(@ntRangefrom = 0, to = 100) int level, boolean pluggedIn)204     public void onBatteryLevelChanged(@IntRange(from = 0, to = 100) int level, boolean pluggedIn) {
205         mDrawable.setCharging(pluggedIn);
206         mDrawable.setBatteryLevel(level);
207         mCharging = pluggedIn;
208         mLevel = level;
209         updatePercentText();
210     }
211 
onPowerSaveChanged(boolean isPowerSave)212     void onPowerSaveChanged(boolean isPowerSave) {
213         mDrawable.setPowerSaveEnabled(isPowerSave);
214     }
215 
onIsOverheatedChanged(boolean isOverheated)216     void onIsOverheatedChanged(boolean isOverheated) {
217         boolean valueChanged = mIsOverheated != isOverheated;
218         mIsOverheated = isOverheated;
219         if (valueChanged) {
220             updateContentDescription();
221             // The battery drawable is a different size depending on whether it's currently
222             // overheated or not, so we need to re-scale the view when overheated changes.
223             scaleBatteryMeterViews();
224         }
225     }
226 
loadPercentView()227     private TextView loadPercentView() {
228         return (TextView) LayoutInflater.from(getContext())
229                 .inflate(R.layout.battery_percentage_view, null);
230     }
231 
232     /**
233      * Updates percent view by removing old one and reinflating if necessary
234      */
updatePercentView()235     public void updatePercentView() {
236         if (mBatteryPercentView != null) {
237             removeView(mBatteryPercentView);
238             mBatteryPercentView = null;
239         }
240         updateShowPercent();
241     }
242 
243     /**
244      * Sets the fetcher that should be used to get the estimated time remaining for the user's
245      * battery.
246      */
setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher)247     void setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher) {
248         mBatteryEstimateFetcher = fetcher;
249     }
250 
setDisplayShieldEnabled(boolean displayShieldEnabled)251     void setDisplayShieldEnabled(boolean displayShieldEnabled) {
252         mDisplayShieldEnabled = displayShieldEnabled;
253     }
254 
updatePercentText()255     void updatePercentText() {
256         if (mBatteryStateUnknown) {
257             return;
258         }
259 
260         if (mBatteryEstimateFetcher == null) {
261             setPercentTextAtCurrentLevel();
262             return;
263         }
264 
265         if (mBatteryPercentView != null) {
266             if (mShowPercentMode == MODE_ESTIMATE && !mCharging) {
267                 mBatteryEstimateFetcher.fetchBatteryTimeRemainingEstimate(
268                         (String estimate) -> {
269                     if (mBatteryPercentView == null) {
270                         return;
271                     }
272                     if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
273                         mEstimateText = estimate;
274                         mBatteryPercentView.setText(estimate);
275                         updateContentDescription();
276                     } else {
277                         setPercentTextAtCurrentLevel();
278                     }
279                 });
280             } else {
281                 setPercentTextAtCurrentLevel();
282             }
283         } else {
284             updateContentDescription();
285         }
286     }
287 
setPercentTextAtCurrentLevel()288     private void setPercentTextAtCurrentLevel() {
289         if (mBatteryPercentView != null) {
290             mEstimateText = null;
291             String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
292             // Setting text actually triggers a layout pass (because the text view is set to
293             // wrap_content width and TextView always relayouts for this). Avoid needless
294             // relayout if the text didn't actually change.
295             if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
296                 mBatteryPercentView.setText(percentText);
297             }
298         }
299 
300         updateContentDescription();
301     }
302 
updateContentDescription()303     private void updateContentDescription() {
304         Context context = getContext();
305 
306         String contentDescription;
307         if (mBatteryStateUnknown) {
308             contentDescription = context.getString(R.string.accessibility_battery_unknown);
309         } else if (mShowPercentMode == MODE_ESTIMATE && !TextUtils.isEmpty(mEstimateText)) {
310             contentDescription = context.getString(
311                     mIsOverheated
312                             ? R.string.accessibility_battery_level_charging_paused_with_estimate
313                             : R.string.accessibility_battery_level_with_estimate,
314                     mLevel,
315                     mEstimateText);
316         } else if (mIsOverheated) {
317             contentDescription =
318                     context.getString(R.string.accessibility_battery_level_charging_paused, mLevel);
319         } else if (mCharging) {
320             contentDescription =
321                     context.getString(R.string.accessibility_battery_level_charging, mLevel);
322         } else {
323             contentDescription = context.getString(R.string.accessibility_battery_level, mLevel);
324         }
325 
326         setContentDescription(contentDescription);
327     }
328 
updateShowPercent()329     void updateShowPercent() {
330         final boolean showing = mBatteryPercentView != null;
331         // TODO(b/140051051)
332         final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System
333                 .getIntForUser(getContext().getContentResolver(),
334                 SHOW_BATTERY_PERCENT, 0, UserHandle.USER_CURRENT));
335         boolean shouldShow =
336                 (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF)
337                 || mShowPercentMode == MODE_ON
338                 || mShowPercentMode == MODE_ESTIMATE;
339         shouldShow = shouldShow && !mBatteryStateUnknown;
340 
341         if (shouldShow) {
342             if (!showing) {
343                 mBatteryPercentView = loadPercentView();
344                 if (mPercentageStyleId != 0) { // Only set if specified as attribute
345                     mBatteryPercentView.setTextAppearance(mPercentageStyleId);
346                 }
347                 if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
348                 updatePercentText();
349                 addView(mBatteryPercentView, new LayoutParams(
350                         LayoutParams.WRAP_CONTENT,
351                         LayoutParams.MATCH_PARENT));
352             }
353         } else {
354             if (showing) {
355                 removeView(mBatteryPercentView);
356                 mBatteryPercentView = null;
357             }
358         }
359     }
360 
getUnknownStateDrawable()361     private Drawable getUnknownStateDrawable() {
362         if (mUnknownStateDrawable == null) {
363             mUnknownStateDrawable = mContext.getDrawable(R.drawable.ic_battery_unknown);
364             mUnknownStateDrawable.setTint(mTextColor);
365         }
366 
367         return mUnknownStateDrawable;
368     }
369 
onBatteryUnknownStateChanged(boolean isUnknown)370     void onBatteryUnknownStateChanged(boolean isUnknown) {
371         if (mBatteryStateUnknown == isUnknown) {
372             return;
373         }
374 
375         mBatteryStateUnknown = isUnknown;
376         updateContentDescription();
377 
378         if (mBatteryStateUnknown) {
379             mBatteryIconView.setImageDrawable(getUnknownStateDrawable());
380         } else {
381             mBatteryIconView.setImageDrawable(mDrawable);
382         }
383 
384         updateShowPercent();
385     }
386 
387     /**
388      * Looks up the scale factor for status bar icons and scales the battery view by that amount.
389      */
scaleBatteryMeterViews()390     void scaleBatteryMeterViews() {
391         Resources res = getContext().getResources();
392         TypedValue typedValue = new TypedValue();
393 
394         res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
395         float iconScaleFactor = typedValue.getFloat();
396 
397         float mainBatteryHeight =
398                 res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height) * iconScaleFactor;
399         float mainBatteryWidth =
400                 res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
401 
402         // If the battery is marked as overheated, we should display a shield indicating that the
403         // battery is being "defended".
404         boolean displayShield = mDisplayShieldEnabled && mIsOverheated;
405         float fullBatteryIconHeight =
406                 BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
407         float fullBatteryIconWidth =
408                 BatterySpecs.getFullBatteryWidth(mainBatteryWidth, displayShield);
409 
410         int marginTop;
411         if (displayShield) {
412             // If the shield is displayed, we need some extra marginTop so that the bottom of the
413             // main icon is still aligned with the bottom of all the other system icons.
414             int shieldHeightAddition = Math.round(fullBatteryIconHeight - mainBatteryHeight);
415             // However, the other system icons have some embedded bottom padding that the battery
416             // doesn't have, so we shouldn't move the battery icon down by the full amount.
417             // See b/258672854.
418             marginTop = shieldHeightAddition
419                     - res.getDimensionPixelSize(R.dimen.status_bar_battery_extra_vertical_spacing);
420         } else {
421             marginTop = 0;
422         }
423 
424         int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
425 
426         LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
427                 Math.round(fullBatteryIconWidth),
428                 Math.round(fullBatteryIconHeight));
429         scaledLayoutParams.setMargins(0, marginTop, 0, marginBottom);
430 
431         mDrawable.setDisplayShield(displayShield);
432         mBatteryIconView.setLayoutParams(scaledLayoutParams);
433         mBatteryIconView.invalidateDrawable(mDrawable);
434     }
435 
436     @Override
onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint)437     public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) {
438         float intensity = DarkIconDispatcher.isInAreas(areas, this) ? darkIntensity : 0;
439         mNonAdaptedSingleToneColor = mDualToneHandler.getSingleColor(intensity);
440         mNonAdaptedForegroundColor = mDualToneHandler.getFillColor(intensity);
441         mNonAdaptedBackgroundColor = mDualToneHandler.getBackgroundColor(intensity);
442 
443         updateColors(mNonAdaptedForegroundColor, mNonAdaptedBackgroundColor,
444                 mNonAdaptedSingleToneColor);
445     }
446 
447     /**
448      * Sets icon and text colors. This will be overridden by {@code onDarkChanged} events,
449      * if registered.
450      *
451      * @param foregroundColor
452      * @param backgroundColor
453      * @param singleToneColor
454      */
updateColors(int foregroundColor, int backgroundColor, int singleToneColor)455     public void updateColors(int foregroundColor, int backgroundColor, int singleToneColor) {
456         mDrawable.setColors(foregroundColor, backgroundColor, singleToneColor);
457         mTextColor = singleToneColor;
458         if (mBatteryPercentView != null) {
459             mBatteryPercentView.setTextColor(singleToneColor);
460         }
461 
462         if (mUnknownStateDrawable != null) {
463             mUnknownStateDrawable.setTint(singleToneColor);
464         }
465     }
466 
dump(PrintWriter pw, String[] args)467     public void dump(PrintWriter pw, String[] args) {
468         String powerSave = mDrawable == null ? null : mDrawable.getPowerSaveEnabled() + "";
469         CharSequence percent = mBatteryPercentView == null ? null : mBatteryPercentView.getText();
470         pw.println("  BatteryMeterView:");
471         pw.println("    mDrawable.getPowerSave: " + powerSave);
472         pw.println("    mBatteryPercentView.getText(): " + percent);
473         pw.println("    mTextColor: #" + Integer.toHexString(mTextColor));
474         pw.println("    mBatteryStateUnknown: " + mBatteryStateUnknown);
475         pw.println("    mLevel: " + mLevel);
476         pw.println("    mMode: " + mShowPercentMode);
477     }
478 
479     @VisibleForTesting
getBatteryPercentViewText()480     CharSequence getBatteryPercentViewText() {
481         return mBatteryPercentView.getText();
482     }
483 
484     /** An interface that will fetch the estimated time remaining for the user's battery. */
485     public interface BatteryEstimateFetcher {
fetchBatteryTimeRemainingEstimate( BatteryController.EstimateFetchCompletion completion)486         void fetchBatteryTimeRemainingEstimate(
487                 BatteryController.EstimateFetchCompletion completion);
488     }
489 }
490 
491