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