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