1 /* 2 * Copyright (C) 2021 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.battery; 17 18 import static android.provider.Settings.System.SHOW_BATTERY_PERCENT; 19 20 import static com.android.systemui.DejankUtils.whitelistIpcs; 21 22 import static java.lang.annotation.RetentionPolicy.SOURCE; 23 24 import android.animation.LayoutTransition; 25 import android.animation.ObjectAnimator; 26 import android.annotation.IntDef; 27 import android.content.Context; 28 import android.content.res.Configuration; 29 import android.content.res.Resources; 30 import android.content.res.TypedArray; 31 import android.graphics.Rect; 32 import android.graphics.drawable.Drawable; 33 import android.os.UserHandle; 34 import android.provider.Settings; 35 import android.util.AttributeSet; 36 import android.util.TypedValue; 37 import android.view.Gravity; 38 import android.view.LayoutInflater; 39 import android.view.ViewGroup; 40 import android.widget.ImageView; 41 import android.widget.LinearLayout; 42 import android.widget.TextView; 43 44 import androidx.annotation.StyleRes; 45 import androidx.annotation.VisibleForTesting; 46 47 import com.android.settingslib.graph.ThemedBatteryDrawable; 48 import com.android.systemui.DualToneHandler; 49 import com.android.systemui.R; 50 import com.android.systemui.animation.Interpolators; 51 import com.android.systemui.plugins.DarkIconDispatcher; 52 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 53 import com.android.systemui.statusbar.policy.BatteryController; 54 55 import java.io.FileDescriptor; 56 import java.io.PrintWriter; 57 import java.lang.annotation.Retention; 58 import java.text.NumberFormat; 59 60 public class BatteryMeterView extends LinearLayout implements DarkReceiver { 61 62 @Retention(SOURCE) 63 @IntDef({MODE_DEFAULT, MODE_ON, MODE_OFF, MODE_ESTIMATE}) 64 public @interface BatteryPercentMode {} 65 public static final int MODE_DEFAULT = 0; 66 public static final int MODE_ON = 1; 67 public static final int MODE_OFF = 2; 68 public static final int MODE_ESTIMATE = 3; 69 70 private final ThemedBatteryDrawable mDrawable; 71 private final ImageView mBatteryIconView; 72 private TextView mBatteryPercentView; 73 74 private final @StyleRes int mPercentageStyleId; 75 private int mTextColor; 76 private int mLevel; 77 private int mShowPercentMode = MODE_DEFAULT; 78 private boolean mShowPercentAvailable; 79 private boolean mCharging; 80 // Error state where we know nothing about the current battery state 81 private boolean mBatteryStateUnknown; 82 // Lazily-loaded since this is expected to be a rare-if-ever state 83 private Drawable mUnknownStateDrawable; 84 85 private DualToneHandler mDualToneHandler; 86 87 private int mNonAdaptedSingleToneColor; 88 private int mNonAdaptedForegroundColor; 89 private int mNonAdaptedBackgroundColor; 90 91 private BatteryEstimateFetcher mBatteryEstimateFetcher; 92 BatteryMeterView(Context context, AttributeSet attrs)93 public BatteryMeterView(Context context, AttributeSet attrs) { 94 this(context, attrs, 0); 95 } 96 BatteryMeterView(Context context, AttributeSet attrs, int defStyle)97 public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) { 98 super(context, attrs, defStyle); 99 100 setOrientation(LinearLayout.HORIZONTAL); 101 setGravity(Gravity.CENTER_VERTICAL | Gravity.START); 102 103 TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView, 104 defStyle, 0); 105 final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor, 106 context.getColor(R.color.meter_background_color)); 107 mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0); 108 mDrawable = new ThemedBatteryDrawable(context, frameColor); 109 atts.recycle(); 110 111 mShowPercentAvailable = context.getResources().getBoolean( 112 com.android.internal.R.bool.config_battery_percentage_setting_available); 113 114 setupLayoutTransition(); 115 116 mBatteryIconView = new ImageView(context); 117 mBatteryIconView.setImageDrawable(mDrawable); 118 final MarginLayoutParams mlp = new MarginLayoutParams( 119 getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_width), 120 getResources().getDimensionPixelSize(R.dimen.status_bar_battery_icon_height)); 121 mlp.setMargins(0, 0, 0, 122 getResources().getDimensionPixelOffset(R.dimen.battery_margin_bottom)); 123 addView(mBatteryIconView, mlp); 124 125 updateShowPercent(); 126 mDualToneHandler = new DualToneHandler(context); 127 // Init to not dark at all. 128 onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT); 129 130 setClipChildren(false); 131 setClipToPadding(false); 132 } 133 setupLayoutTransition()134 private void setupLayoutTransition() { 135 LayoutTransition transition = new LayoutTransition(); 136 transition.setDuration(200); 137 138 ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); 139 transition.setAnimator(LayoutTransition.APPEARING, appearAnimator); 140 transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN); 141 142 ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f); 143 transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT); 144 transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator); 145 146 setLayoutTransition(transition); 147 } 148 setForceShowPercent(boolean show)149 public void setForceShowPercent(boolean show) { 150 setPercentShowMode(show ? MODE_ON : MODE_DEFAULT); 151 } 152 153 /** 154 * Force a particular mode of showing percent 155 * 156 * 0 - No preference 157 * 1 - Force on 158 * 2 - Force off 159 * 3 - Estimate 160 * @param mode desired mode (none, on, off) 161 */ setPercentShowMode(@atteryPercentMode int mode)162 public void setPercentShowMode(@BatteryPercentMode int mode) { 163 if (mode == mShowPercentMode) return; 164 mShowPercentMode = mode; 165 updateShowPercent(); 166 } 167 168 @Override onConfigurationChanged(Configuration newConfig)169 protected void onConfigurationChanged(Configuration newConfig) { 170 super.onConfigurationChanged(newConfig); 171 updatePercentView(); 172 } 173 setColorsFromContext(Context context)174 public void setColorsFromContext(Context context) { 175 if (context == null) { 176 return; 177 } 178 179 mDualToneHandler.setColorsFromContext(context); 180 } 181 182 @Override hasOverlappingRendering()183 public boolean hasOverlappingRendering() { 184 return false; 185 } 186 onBatteryLevelChanged(int level, boolean pluggedIn)187 void onBatteryLevelChanged(int level, boolean pluggedIn) { 188 mDrawable.setCharging(pluggedIn); 189 mDrawable.setBatteryLevel(level); 190 mCharging = pluggedIn; 191 mLevel = level; 192 updatePercentText(); 193 } 194 onPowerSaveChanged(boolean isPowerSave)195 void onPowerSaveChanged(boolean isPowerSave) { 196 mDrawable.setPowerSaveEnabled(isPowerSave); 197 } 198 loadPercentView()199 private TextView loadPercentView() { 200 return (TextView) LayoutInflater.from(getContext()) 201 .inflate(R.layout.battery_percentage_view, null); 202 } 203 204 /** 205 * Updates percent view by removing old one and reinflating if necessary 206 */ updatePercentView()207 public void updatePercentView() { 208 if (mBatteryPercentView != null) { 209 removeView(mBatteryPercentView); 210 mBatteryPercentView = null; 211 } 212 updateShowPercent(); 213 } 214 215 /** 216 * Sets the fetcher that should be used to get the estimated time remaining for the user's 217 * battery. 218 */ setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher)219 void setBatteryEstimateFetcher(BatteryEstimateFetcher fetcher) { 220 mBatteryEstimateFetcher = fetcher; 221 } 222 updatePercentText()223 void updatePercentText() { 224 if (mBatteryStateUnknown) { 225 setContentDescription(getContext().getString(R.string.accessibility_battery_unknown)); 226 return; 227 } 228 229 if (mBatteryEstimateFetcher == null) { 230 return; 231 } 232 233 if (mBatteryPercentView != null) { 234 if (mShowPercentMode == MODE_ESTIMATE && !mCharging) { 235 mBatteryEstimateFetcher.fetchBatteryTimeRemainingEstimate( 236 (String estimate) -> { 237 if (mBatteryPercentView == null) { 238 return; 239 } 240 if (estimate != null && mShowPercentMode == MODE_ESTIMATE) { 241 mBatteryPercentView.setText(estimate); 242 setContentDescription(getContext().getString( 243 R.string.accessibility_battery_level_with_estimate, 244 mLevel, estimate)); 245 } else { 246 setPercentTextAtCurrentLevel(); 247 } 248 }); 249 } else { 250 setPercentTextAtCurrentLevel(); 251 } 252 } else { 253 setContentDescription( 254 getContext().getString(mCharging ? R.string.accessibility_battery_level_charging 255 : R.string.accessibility_battery_level, mLevel)); 256 } 257 } 258 setPercentTextAtCurrentLevel()259 private void setPercentTextAtCurrentLevel() { 260 if (mBatteryPercentView == null) { 261 return; 262 } 263 mBatteryPercentView.setText( 264 NumberFormat.getPercentInstance().format(mLevel / 100f)); 265 setContentDescription( 266 getContext().getString(mCharging ? R.string.accessibility_battery_level_charging 267 : R.string.accessibility_battery_level, mLevel)); 268 } 269 updateShowPercent()270 void updateShowPercent() { 271 final boolean showing = mBatteryPercentView != null; 272 // TODO(b/140051051) 273 final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System 274 .getIntForUser(getContext().getContentResolver(), 275 SHOW_BATTERY_PERCENT, 0, UserHandle.USER_CURRENT)); 276 boolean shouldShow = 277 (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF) 278 || mShowPercentMode == MODE_ON 279 || mShowPercentMode == MODE_ESTIMATE; 280 shouldShow = shouldShow && !mBatteryStateUnknown; 281 282 if (shouldShow) { 283 if (!showing) { 284 mBatteryPercentView = loadPercentView(); 285 if (mPercentageStyleId != 0) { // Only set if specified as attribute 286 mBatteryPercentView.setTextAppearance(mPercentageStyleId); 287 } 288 if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor); 289 updatePercentText(); 290 addView(mBatteryPercentView, 291 new ViewGroup.LayoutParams( 292 LayoutParams.WRAP_CONTENT, 293 LayoutParams.MATCH_PARENT)); 294 } 295 } else { 296 if (showing) { 297 removeView(mBatteryPercentView); 298 mBatteryPercentView = null; 299 } 300 } 301 } 302 getUnknownStateDrawable()303 private Drawable getUnknownStateDrawable() { 304 if (mUnknownStateDrawable == null) { 305 mUnknownStateDrawable = mContext.getDrawable(R.drawable.ic_battery_unknown); 306 mUnknownStateDrawable.setTint(mTextColor); 307 } 308 309 return mUnknownStateDrawable; 310 } 311 onBatteryUnknownStateChanged(boolean isUnknown)312 void onBatteryUnknownStateChanged(boolean isUnknown) { 313 if (mBatteryStateUnknown == isUnknown) { 314 return; 315 } 316 317 mBatteryStateUnknown = isUnknown; 318 319 if (mBatteryStateUnknown) { 320 mBatteryIconView.setImageDrawable(getUnknownStateDrawable()); 321 } else { 322 mBatteryIconView.setImageDrawable(mDrawable); 323 } 324 325 updateShowPercent(); 326 } 327 328 /** 329 * Looks up the scale factor for status bar icons and scales the battery view by that amount. 330 */ scaleBatteryMeterViews()331 void scaleBatteryMeterViews() { 332 Resources res = getContext().getResources(); 333 TypedValue typedValue = new TypedValue(); 334 335 res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true); 336 float iconScaleFactor = typedValue.getFloat(); 337 338 int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height); 339 int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width); 340 int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom); 341 342 LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams( 343 (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor)); 344 scaledLayoutParams.setMargins(0, 0, 0, marginBottom); 345 346 mBatteryIconView.setLayoutParams(scaledLayoutParams); 347 } 348 349 @Override onDarkChanged(Rect area, float darkIntensity, int tint)350 public void onDarkChanged(Rect area, float darkIntensity, int tint) { 351 float intensity = DarkIconDispatcher.isInArea(area, this) ? darkIntensity : 0; 352 mNonAdaptedSingleToneColor = mDualToneHandler.getSingleColor(intensity); 353 mNonAdaptedForegroundColor = mDualToneHandler.getFillColor(intensity); 354 mNonAdaptedBackgroundColor = mDualToneHandler.getBackgroundColor(intensity); 355 356 updateColors(mNonAdaptedForegroundColor, mNonAdaptedBackgroundColor, 357 mNonAdaptedSingleToneColor); 358 } 359 360 /** 361 * Sets icon and text colors. This will be overridden by {@code onDarkChanged} events, 362 * if registered. 363 * 364 * @param foregroundColor 365 * @param backgroundColor 366 * @param singleToneColor 367 */ updateColors(int foregroundColor, int backgroundColor, int singleToneColor)368 public void updateColors(int foregroundColor, int backgroundColor, int singleToneColor) { 369 mDrawable.setColors(foregroundColor, backgroundColor, singleToneColor); 370 mTextColor = singleToneColor; 371 if (mBatteryPercentView != null) { 372 mBatteryPercentView.setTextColor(singleToneColor); 373 } 374 375 if (mUnknownStateDrawable != null) { 376 mUnknownStateDrawable.setTint(singleToneColor); 377 } 378 } 379 dump(FileDescriptor fd, PrintWriter pw, String[] args)380 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 381 String powerSave = mDrawable == null ? null : mDrawable.getPowerSaveEnabled() + ""; 382 CharSequence percent = mBatteryPercentView == null ? null : mBatteryPercentView.getText(); 383 pw.println(" BatteryMeterView:"); 384 pw.println(" mDrawable.getPowerSave: " + powerSave); 385 pw.println(" mBatteryPercentView.getText(): " + percent); 386 pw.println(" mTextColor: #" + Integer.toHexString(mTextColor)); 387 pw.println(" mBatteryStateUnknown: " + mBatteryStateUnknown); 388 pw.println(" mLevel: " + mLevel); 389 pw.println(" mMode: " + mShowPercentMode); 390 } 391 392 @VisibleForTesting getBatteryPercentViewText()393 CharSequence getBatteryPercentViewText() { 394 return mBatteryPercentView.getText(); 395 } 396 397 /** An interface that will fetch the estimated time remaining for the user's battery. */ 398 public interface BatteryEstimateFetcher { fetchBatteryTimeRemainingEstimate( BatteryController.EstimateFetchCompletion completion)399 void fetchBatteryTimeRemainingEstimate( 400 BatteryController.EstimateFetchCompletion completion); 401 } 402 } 403 404