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