• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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;
17 
18 import static android.provider.Settings.System.SHOW_BATTERY_PERCENT;
19 
20 import static com.android.systemui.DejankUtils.whitelistIpcs;
21 import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
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.app.ActivityManager;
29 import android.content.Context;
30 import android.content.res.Resources;
31 import android.content.res.TypedArray;
32 import android.database.ContentObserver;
33 import android.graphics.Rect;
34 import android.graphics.drawable.Drawable;
35 import android.net.Uri;
36 import android.os.Handler;
37 import android.provider.Settings;
38 import android.text.TextUtils;
39 import android.util.ArraySet;
40 import android.util.AttributeSet;
41 import android.util.TypedValue;
42 import android.view.Gravity;
43 import android.view.LayoutInflater;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.widget.ImageView;
47 import android.widget.LinearLayout;
48 import android.widget.TextView;
49 
50 import androidx.annotation.StyleRes;
51 
52 import com.android.settingslib.graph.ThemedBatteryDrawable;
53 import com.android.systemui.animation.Interpolators;
54 import com.android.systemui.broadcast.BroadcastDispatcher;
55 import com.android.systemui.plugins.DarkIconDispatcher;
56 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
57 import com.android.systemui.settings.CurrentUserTracker;
58 import com.android.systemui.statusbar.phone.StatusBarIconController;
59 import com.android.systemui.statusbar.policy.BatteryController;
60 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
61 import com.android.systemui.statusbar.policy.ConfigurationController;
62 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
63 import com.android.systemui.tuner.TunerService;
64 import com.android.systemui.tuner.TunerService.Tunable;
65 
66 import java.io.FileDescriptor;
67 import java.io.PrintWriter;
68 import java.lang.annotation.Retention;
69 import java.text.NumberFormat;
70 
71 public class BatteryMeterView extends LinearLayout implements
72         BatteryStateChangeCallback, Tunable, DarkReceiver, ConfigurationListener {
73 
74 
75     @Retention(SOURCE)
76     @IntDef({MODE_DEFAULT, MODE_ON, MODE_OFF, MODE_ESTIMATE})
77     public @interface BatteryPercentMode {}
78     public static final int MODE_DEFAULT = 0;
79     public static final int MODE_ON = 1;
80     public static final int MODE_OFF = 2;
81     public static final int MODE_ESTIMATE = 3;
82 
83     private final ThemedBatteryDrawable mDrawable;
84     private final String mSlotBattery;
85     private final ImageView mBatteryIconView;
86     private final CurrentUserTracker mUserTracker;
87     private TextView mBatteryPercentView;
88 
89     private BatteryController mBatteryController;
90     private SettingObserver mSettingObserver;
91     private final @StyleRes int mPercentageStyleId;
92     private int mTextColor;
93     private int mLevel;
94     private int mShowPercentMode = MODE_DEFAULT;
95     private boolean mShowPercentAvailable;
96     // Some places may need to show the battery conditionally, and not obey the tuner
97     private boolean mIgnoreTunerUpdates;
98     private boolean mIsSubscribedForTunerUpdates;
99     private boolean mCharging;
100     // Error state where we know nothing about the current battery state
101     private boolean mBatteryStateUnknown;
102     // Lazily-loaded since this is expected to be a rare-if-ever state
103     private Drawable mUnknownStateDrawable;
104 
105     private DualToneHandler mDualToneHandler;
106     private int mUser;
107 
108     private int mNonAdaptedSingleToneColor;
109     private int mNonAdaptedForegroundColor;
110     private int mNonAdaptedBackgroundColor;
111 
BatteryMeterView(Context context, AttributeSet attrs)112     public BatteryMeterView(Context context, AttributeSet attrs) {
113         this(context, attrs, 0);
114     }
115 
BatteryMeterView(Context context, AttributeSet attrs, int defStyle)116     public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) {
117         super(context, attrs, defStyle);
118         BroadcastDispatcher broadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
119 
120         setOrientation(LinearLayout.HORIZONTAL);
121         setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
122 
123         TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView,
124                 defStyle, 0);
125         final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
126                 context.getColor(R.color.meter_background_color));
127         mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0);
128         mDrawable = new ThemedBatteryDrawable(context, frameColor);
129         atts.recycle();
130 
131         mSettingObserver = new SettingObserver(new Handler(context.getMainLooper()));
132         mShowPercentAvailable = context.getResources().getBoolean(
133                 com.android.internal.R.bool.config_battery_percentage_setting_available);
134 
135         setupLayoutTransition();
136 
137         mSlotBattery = context.getString(
138                 com.android.internal.R.string.status_bar_battery);
139         mBatteryIconView = new ImageView(context);
140         mBatteryIconView.setImageDrawable(mDrawable);
141         final MarginLayoutParams mlp = new MarginLayoutParams(
142                 getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_width),
143                 getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_height));
144         mlp.setMargins(0, 0, 0,
145                 getResources().getDimensionPixelOffset(R.dimen.battery_margin_bottom));
146         addView(mBatteryIconView, mlp);
147 
148         updateShowPercent();
149         mDualToneHandler = new DualToneHandler(context);
150         // Init to not dark at all.
151         onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
152 
153         mUserTracker = new CurrentUserTracker(broadcastDispatcher) {
154             @Override
155             public void onUserSwitched(int newUserId) {
156                 mUser = newUserId;
157                 getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
158                 getContext().getContentResolver().registerContentObserver(
159                         Settings.System.getUriFor(SHOW_BATTERY_PERCENT), false, mSettingObserver,
160                         newUserId);
161                 updateShowPercent();
162             }
163         };
164 
165         setClipChildren(false);
166         setClipToPadding(false);
167         Dependency.get(ConfigurationController.class).observe(viewAttachLifecycle(this), this);
168     }
169 
setupLayoutTransition()170     private void setupLayoutTransition() {
171         LayoutTransition transition = new LayoutTransition();
172         transition.setDuration(200);
173 
174         ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
175         transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
176         transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
177 
178         ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
179         transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
180         transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
181 
182         setLayoutTransition(transition);
183     }
184 
setForceShowPercent(boolean show)185     public void setForceShowPercent(boolean show) {
186         setPercentShowMode(show ? MODE_ON : MODE_DEFAULT);
187     }
188 
189     /**
190      * Force a particular mode of showing percent
191      *
192      * 0 - No preference
193      * 1 - Force on
194      * 2 - Force off
195      * @param mode desired mode (none, on, off)
196      */
setPercentShowMode(@atteryPercentMode int mode)197     public void setPercentShowMode(@BatteryPercentMode int mode) {
198         if (mode == mShowPercentMode) return;
199         mShowPercentMode = mode;
200         updateShowPercent();
201     }
202 
203     /**
204      * Set {@code true} to turn off BatteryMeterView's subscribing to the tuner for updates, and
205      * thus avoid it controlling its own visibility
206      *
207      * @param ignore whether to ignore the tuner or not
208      */
setIgnoreTunerUpdates(boolean ignore)209     public void setIgnoreTunerUpdates(boolean ignore) {
210         mIgnoreTunerUpdates = ignore;
211         updateTunerSubscription();
212     }
213 
updateTunerSubscription()214     private void updateTunerSubscription() {
215         if (mIgnoreTunerUpdates) {
216             unsubscribeFromTunerUpdates();
217         } else {
218             subscribeForTunerUpdates();
219         }
220     }
221 
subscribeForTunerUpdates()222     private void subscribeForTunerUpdates() {
223         if (mIsSubscribedForTunerUpdates || mIgnoreTunerUpdates) {
224             return;
225         }
226 
227         Dependency.get(TunerService.class)
228                 .addTunable(this, StatusBarIconController.ICON_HIDE_LIST);
229         mIsSubscribedForTunerUpdates = true;
230     }
231 
unsubscribeFromTunerUpdates()232     private void unsubscribeFromTunerUpdates() {
233         if (!mIsSubscribedForTunerUpdates) {
234             return;
235         }
236 
237         Dependency.get(TunerService.class).removeTunable(this);
238         mIsSubscribedForTunerUpdates = false;
239     }
240 
setColorsFromContext(Context context)241     public void setColorsFromContext(Context context) {
242         if (context == null) {
243             return;
244         }
245 
246         mDualToneHandler.setColorsFromContext(context);
247     }
248 
249     @Override
hasOverlappingRendering()250     public boolean hasOverlappingRendering() {
251         return false;
252     }
253 
254     @Override
onTuningChanged(String key, String newValue)255     public void onTuningChanged(String key, String newValue) {
256         if (StatusBarIconController.ICON_HIDE_LIST.equals(key)) {
257             ArraySet<String> icons = StatusBarIconController.getIconHideList(
258                     getContext(), newValue);
259             setVisibility(icons.contains(mSlotBattery) ? View.GONE : View.VISIBLE);
260         }
261     }
262 
263     @Override
onAttachedToWindow()264     public void onAttachedToWindow() {
265         super.onAttachedToWindow();
266         mBatteryController = Dependency.get(BatteryController.class);
267         mBatteryController.addCallback(this);
268         mUser = ActivityManager.getCurrentUser();
269         getContext().getContentResolver().registerContentObserver(
270                 Settings.System.getUriFor(SHOW_BATTERY_PERCENT), false, mSettingObserver, mUser);
271         getContext().getContentResolver().registerContentObserver(
272                 Settings.Global.getUriFor(Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME),
273                 false, mSettingObserver);
274         updateShowPercent();
275         subscribeForTunerUpdates();
276         mUserTracker.startTracking();
277     }
278 
279     @Override
onDetachedFromWindow()280     public void onDetachedFromWindow() {
281         super.onDetachedFromWindow();
282         mUserTracker.stopTracking();
283         mBatteryController.removeCallback(this);
284         getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
285         unsubscribeFromTunerUpdates();
286     }
287 
288     @Override
onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging)289     public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
290         mDrawable.setCharging(pluggedIn);
291         mDrawable.setBatteryLevel(level);
292         mCharging = pluggedIn;
293         mLevel = level;
294         updatePercentText();
295     }
296 
297     @Override
onPowerSaveChanged(boolean isPowerSave)298     public void onPowerSaveChanged(boolean isPowerSave) {
299         mDrawable.setPowerSaveEnabled(isPowerSave);
300     }
301 
loadPercentView()302     private TextView loadPercentView() {
303         return (TextView) LayoutInflater.from(getContext())
304                 .inflate(R.layout.battery_percentage_view, null);
305     }
306 
307     /**
308      * Updates percent view by removing old one and reinflating if necessary
309      */
updatePercentView()310     public void updatePercentView() {
311         if (mBatteryPercentView != null) {
312             removeView(mBatteryPercentView);
313             mBatteryPercentView = null;
314         }
315         updateShowPercent();
316     }
317 
updatePercentText()318     private void updatePercentText() {
319         if (mBatteryStateUnknown) {
320             setContentDescription(getContext().getString(R.string.accessibility_battery_unknown));
321             return;
322         }
323 
324         if (mBatteryController == null) {
325             return;
326         }
327 
328         if (mBatteryPercentView != null) {
329             if (mShowPercentMode == MODE_ESTIMATE && !mCharging) {
330                 mBatteryController.getEstimatedTimeRemainingString((String estimate) -> {
331                     if (mBatteryPercentView == null) {
332                         return;
333                     }
334                     if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
335                         mBatteryPercentView.setText(estimate);
336                         setContentDescription(getContext().getString(
337                                 R.string.accessibility_battery_level_with_estimate,
338                                 mLevel, estimate));
339                     } else {
340                         setPercentTextAtCurrentLevel();
341                     }
342                 });
343             } else {
344                 setPercentTextAtCurrentLevel();
345             }
346         } else {
347             setContentDescription(
348                     getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
349                             : R.string.accessibility_battery_level, mLevel));
350         }
351     }
352 
setPercentTextAtCurrentLevel()353     private void setPercentTextAtCurrentLevel() {
354         if (mBatteryPercentView == null) {
355             return;
356         }
357         mBatteryPercentView.setText(
358                 NumberFormat.getPercentInstance().format(mLevel / 100f));
359         setContentDescription(
360                 getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
361                         : R.string.accessibility_battery_level, mLevel));
362     }
363 
updateShowPercent()364     private void updateShowPercent() {
365         final boolean showing = mBatteryPercentView != null;
366         // TODO(b/140051051)
367         final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System
368                 .getIntForUser(getContext().getContentResolver(),
369                 SHOW_BATTERY_PERCENT, 0, mUser));
370         boolean shouldShow =
371                 (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF)
372                 || mShowPercentMode == MODE_ON
373                 || mShowPercentMode == MODE_ESTIMATE;
374         shouldShow = shouldShow && !mBatteryStateUnknown;
375 
376         if (shouldShow) {
377             if (!showing) {
378                 mBatteryPercentView = loadPercentView();
379                 if (mPercentageStyleId != 0) { // Only set if specified as attribute
380                     mBatteryPercentView.setTextAppearance(mPercentageStyleId);
381                 }
382                 if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
383                 updatePercentText();
384                 addView(mBatteryPercentView,
385                         new ViewGroup.LayoutParams(
386                                 LayoutParams.WRAP_CONTENT,
387                                 LayoutParams.MATCH_PARENT));
388             }
389         } else {
390             if (showing) {
391                 removeView(mBatteryPercentView);
392                 mBatteryPercentView = null;
393             }
394         }
395     }
396 
397     @Override
onDensityOrFontScaleChanged()398     public void onDensityOrFontScaleChanged() {
399         scaleBatteryMeterViews();
400     }
401 
getUnknownStateDrawable()402     private Drawable getUnknownStateDrawable() {
403         if (mUnknownStateDrawable == null) {
404             mUnknownStateDrawable = mContext.getDrawable(R.drawable.ic_battery_unknown);
405             mUnknownStateDrawable.setTint(mTextColor);
406         }
407 
408         return mUnknownStateDrawable;
409     }
410 
411     @Override
onBatteryUnknownStateChanged(boolean isUnknown)412     public void onBatteryUnknownStateChanged(boolean isUnknown) {
413         if (mBatteryStateUnknown == isUnknown) {
414             return;
415         }
416 
417         mBatteryStateUnknown = isUnknown;
418 
419         if (mBatteryStateUnknown) {
420             mBatteryIconView.setImageDrawable(getUnknownStateDrawable());
421         } else {
422             mBatteryIconView.setImageDrawable(mDrawable);
423         }
424 
425         updateShowPercent();
426     }
427 
428     /**
429      * Looks up the scale factor for status bar icons and scales the battery view by that amount.
430      */
scaleBatteryMeterViews()431     private void scaleBatteryMeterViews() {
432         Resources res = getContext().getResources();
433         TypedValue typedValue = new TypedValue();
434 
435         res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
436         float iconScaleFactor = typedValue.getFloat();
437 
438         int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height);
439         int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width);
440         int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
441 
442         LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
443                 (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
444         scaledLayoutParams.setMargins(0, 0, 0, marginBottom);
445 
446         mBatteryIconView.setLayoutParams(scaledLayoutParams);
447     }
448 
449     @Override
onDarkChanged(Rect area, float darkIntensity, int tint)450     public void onDarkChanged(Rect area, float darkIntensity, int tint) {
451         float intensity = DarkIconDispatcher.isInArea(area, this) ? darkIntensity : 0;
452         mNonAdaptedSingleToneColor = mDualToneHandler.getSingleColor(intensity);
453         mNonAdaptedForegroundColor = mDualToneHandler.getFillColor(intensity);
454         mNonAdaptedBackgroundColor = mDualToneHandler.getBackgroundColor(intensity);
455 
456         updateColors(mNonAdaptedForegroundColor, mNonAdaptedBackgroundColor,
457                 mNonAdaptedSingleToneColor);
458     }
459 
460     /**
461      * Sets icon and text colors. This will be overridden by {@code onDarkChanged} events,
462      * if registered.
463      *
464      * @param foregroundColor
465      * @param backgroundColor
466      * @param singleToneColor
467      */
updateColors(int foregroundColor, int backgroundColor, int singleToneColor)468     public void updateColors(int foregroundColor, int backgroundColor, int singleToneColor) {
469         mDrawable.setColors(foregroundColor, backgroundColor, singleToneColor);
470         mTextColor = singleToneColor;
471         if (mBatteryPercentView != null) {
472             mBatteryPercentView.setTextColor(singleToneColor);
473         }
474 
475         if (mUnknownStateDrawable != null) {
476             mUnknownStateDrawable.setTint(singleToneColor);
477         }
478     }
479 
dump(FileDescriptor fd, PrintWriter pw, String[] args)480     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
481         String powerSave = mDrawable == null ? null : mDrawable.getPowerSaveEnabled() + "";
482         CharSequence percent = mBatteryPercentView == null ? null : mBatteryPercentView.getText();
483         pw.println("  BatteryMeterView:");
484         pw.println("    mDrawable.getPowerSave: " + powerSave);
485         pw.println("    mBatteryPercentView.getText(): " + percent);
486         pw.println("    mTextColor: #" + Integer.toHexString(mTextColor));
487         pw.println("    mBatteryStateUnknown: " + mBatteryStateUnknown);
488         pw.println("    mLevel: " + mLevel);
489         pw.println("    mMode: " + mShowPercentMode);
490     }
491 
492     private final class SettingObserver extends ContentObserver {
SettingObserver(Handler handler)493         public SettingObserver(Handler handler) {
494             super(handler);
495         }
496 
497         @Override
onChange(boolean selfChange, Uri uri)498         public void onChange(boolean selfChange, Uri uri) {
499             super.onChange(selfChange, uri);
500             updateShowPercent();
501             if (TextUtils.equals(uri.getLastPathSegment(),
502                     Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME)) {
503                 // update the text for sure if the estimate in the cache was updated
504                 updatePercentText();
505             }
506         }
507     }
508 }
509