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