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