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