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