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 17 package com.android.systemui; 18 19 import android.animation.ArgbEvaluator; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.res.Resources; 25 import android.content.res.TypedArray; 26 import android.database.ContentObserver; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.Paint; 30 import android.graphics.Path; 31 import android.graphics.RectF; 32 import android.graphics.Typeface; 33 import android.net.Uri; 34 import android.os.BatteryManager; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.provider.Settings; 38 import android.util.AttributeSet; 39 import android.view.View; 40 41 import com.android.systemui.statusbar.policy.BatteryController; 42 43 public class BatteryMeterView extends View implements DemoMode, 44 BatteryController.BatteryStateChangeCallback { 45 public static final String TAG = BatteryMeterView.class.getSimpleName(); 46 public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST"; 47 public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent"; 48 49 private static final boolean SINGLE_DIGIT_PERCENT = false; 50 51 private static final int FULL = 96; 52 53 private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction 54 55 private final int[] mColors; 56 57 private boolean mShowPercent; 58 private float mButtonHeightFraction; 59 private float mSubpixelSmoothingLeft; 60 private float mSubpixelSmoothingRight; 61 private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint; 62 private float mTextHeight, mWarningTextHeight; 63 private int mIconTint = Color.WHITE; 64 65 private int mHeight; 66 private int mWidth; 67 private String mWarningString; 68 private final int mCriticalLevel; 69 private int mChargeColor; 70 private final float[] mBoltPoints; 71 private final Path mBoltPath = new Path(); 72 73 private final RectF mFrame = new RectF(); 74 private final RectF mButtonFrame = new RectF(); 75 private final RectF mBoltFrame = new RectF(); 76 77 private final Path mShapePath = new Path(); 78 private final Path mClipPath = new Path(); 79 private final Path mTextPath = new Path(); 80 81 private BatteryController mBatteryController; 82 private boolean mPowerSaveEnabled; 83 84 private int mDarkModeBackgroundColor; 85 private int mDarkModeFillColor; 86 87 private int mLightModeBackgroundColor; 88 private int mLightModeFillColor; 89 90 private BatteryTracker mTracker = new BatteryTracker(); 91 private final SettingObserver mSettingObserver = new SettingObserver(); 92 93 public BatteryMeterView(Context context) { 94 this(context, null, 0); 95 } 96 97 public BatteryMeterView(Context context, AttributeSet attrs) { 98 this(context, attrs, 0); 99 } 100 101 public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) { 102 super(context, attrs, defStyle); 103 104 final Resources res = context.getResources(); 105 TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView, 106 defStyle, 0); 107 final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor, 108 context.getColor(R.color.batterymeter_frame_color)); 109 TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels); 110 TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values); 111 112 final int N = levels.length(); 113 mColors = new int[2*N]; 114 for (int i=0; i<N; i++) { 115 mColors[2*i] = levels.getInt(i, 0); 116 mColors[2*i+1] = colors.getColor(i, 0); 117 } 118 levels.recycle(); 119 colors.recycle(); 120 atts.recycle(); 121 updateShowPercent(); 122 mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol); 123 mCriticalLevel = mContext.getResources().getInteger( 124 com.android.internal.R.integer.config_criticalBatteryWarningLevel); 125 mButtonHeightFraction = context.getResources().getFraction( 126 R.fraction.battery_button_height_fraction, 1, 1); 127 mSubpixelSmoothingLeft = context.getResources().getFraction( 128 R.fraction.battery_subpixel_smoothing_left, 1, 1); 129 mSubpixelSmoothingRight = context.getResources().getFraction( 130 R.fraction.battery_subpixel_smoothing_right, 1, 1); 131 132 mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 133 mFramePaint.setColor(frameColor); 134 mFramePaint.setDither(true); 135 mFramePaint.setStrokeWidth(0); 136 mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE); 137 138 mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 139 mBatteryPaint.setDither(true); 140 mBatteryPaint.setStrokeWidth(0); 141 mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE); 142 143 mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 144 Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD); 145 mTextPaint.setTypeface(font); 146 mTextPaint.setTextAlign(Paint.Align.CENTER); 147 148 mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 149 mWarningTextPaint.setColor(mColors[1]); 150 font = Typeface.create("sans-serif", Typeface.BOLD); 151 mWarningTextPaint.setTypeface(font); 152 mWarningTextPaint.setTextAlign(Paint.Align.CENTER); 153 154 mChargeColor = context.getColor(R.color.batterymeter_charge_color); 155 156 mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 157 mBoltPaint.setColor(context.getColor(R.color.batterymeter_bolt_color)); 158 mBoltPoints = loadBoltPoints(res); 159 160 mDarkModeBackgroundColor = 161 context.getColor(R.color.dark_mode_icon_color_dual_tone_background); 162 mDarkModeFillColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_fill); 163 mLightModeBackgroundColor = 164 context.getColor(R.color.light_mode_icon_color_dual_tone_background); 165 mLightModeFillColor = context.getColor(R.color.light_mode_icon_color_dual_tone_fill); 166 } 167 168 @Override 169 public void onAttachedToWindow() { 170 super.onAttachedToWindow(); 171 172 IntentFilter filter = new IntentFilter(); 173 filter.addAction(Intent.ACTION_BATTERY_CHANGED); 174 filter.addAction(ACTION_LEVEL_TEST); 175 final Intent sticky = getContext().registerReceiver(mTracker, filter); 176 if (sticky != null) { 177 // preload the battery level 178 mTracker.onReceive(getContext(), sticky); 179 } 180 mBatteryController.addStateChangedCallback(this); 181 getContext().getContentResolver().registerContentObserver( 182 Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver); 183 } 184 185 @Override 186 public void onDetachedFromWindow() { 187 super.onDetachedFromWindow(); 188 189 getContext().unregisterReceiver(mTracker); 190 mBatteryController.removeStateChangedCallback(this); 191 getContext().getContentResolver().unregisterContentObserver(mSettingObserver); 192 } 193 194 public void setBatteryController(BatteryController batteryController) { 195 mBatteryController = batteryController; 196 mPowerSaveEnabled = mBatteryController.isPowerSave(); 197 } 198 199 @Override 200 public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { 201 // TODO: Use this callback instead of own broadcast receiver. 202 } 203 204 @Override 205 public void onPowerSaveChanged() { 206 mPowerSaveEnabled = mBatteryController.isPowerSave(); 207 invalidate(); 208 } 209 210 private static float[] loadBoltPoints(Resources res) { 211 final int[] pts = res.getIntArray(R.array.batterymeter_bolt_points); 212 int maxX = 0, maxY = 0; 213 for (int i = 0; i < pts.length; i += 2) { 214 maxX = Math.max(maxX, pts[i]); 215 maxY = Math.max(maxY, pts[i + 1]); 216 } 217 final float[] ptsF = new float[pts.length]; 218 for (int i = 0; i < pts.length; i += 2) { 219 ptsF[i] = (float)pts[i] / maxX; 220 ptsF[i + 1] = (float)pts[i + 1] / maxY; 221 } 222 return ptsF; 223 } 224 225 @Override 226 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 227 mHeight = h; 228 mWidth = w; 229 mWarningTextPaint.setTextSize(h * 0.75f); 230 mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent; 231 } 232 233 private void updateShowPercent() { 234 mShowPercent = 0 != Settings.System.getInt(getContext().getContentResolver(), 235 SHOW_PERCENT_SETTING, 0); 236 } 237 238 private int getColorForLevel(int percent) { 239 240 // If we are in power save mode, always use the normal color. 241 if (mPowerSaveEnabled) { 242 return mColors[mColors.length-1]; 243 } 244 int thresh, color = 0; 245 for (int i=0; i<mColors.length; i+=2) { 246 thresh = mColors[i]; 247 color = mColors[i+1]; 248 if (percent <= thresh) { 249 250 // Respect tinting for "normal" level 251 if (i == mColors.length-2) { 252 return mIconTint; 253 } else { 254 return color; 255 } 256 } 257 } 258 return color; 259 } 260 261 public void setDarkIntensity(float darkIntensity) { 262 int backgroundColor = getBackgroundColor(darkIntensity); 263 int fillColor = getFillColor(darkIntensity); 264 mIconTint = fillColor; 265 mFramePaint.setColor(backgroundColor); 266 mBoltPaint.setColor(fillColor); 267 mChargeColor = fillColor; 268 invalidate(); 269 } 270 271 private int getBackgroundColor(float darkIntensity) { 272 return getColorForDarkIntensity( 273 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor); 274 } 275 276 private int getFillColor(float darkIntensity) { 277 return getColorForDarkIntensity( 278 darkIntensity, mLightModeFillColor, mDarkModeFillColor); 279 } 280 281 private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) { 282 return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor); 283 } 284 285 @Override 286 public void draw(Canvas c) { 287 BatteryTracker tracker = mDemoMode ? mDemoTracker : mTracker; 288 final int level = tracker.level; 289 290 if (level == BatteryTracker.UNKNOWN_LEVEL) return; 291 292 float drawFrac = (float) level / 100f; 293 final int pt = getPaddingTop(); 294 final int pl = getPaddingLeft(); 295 final int pr = getPaddingRight(); 296 final int pb = getPaddingBottom(); 297 final int height = mHeight - pt - pb; 298 final int width = mWidth - pl - pr; 299 300 final int buttonHeight = (int) (height * mButtonHeightFraction); 301 302 mFrame.set(0, 0, width, height); 303 mFrame.offset(pl, pt); 304 305 // button-frame: area above the battery body 306 mButtonFrame.set( 307 mFrame.left + Math.round(width * 0.25f), 308 mFrame.top, 309 mFrame.right - Math.round(width * 0.25f), 310 mFrame.top + buttonHeight); 311 312 mButtonFrame.top += mSubpixelSmoothingLeft; 313 mButtonFrame.left += mSubpixelSmoothingLeft; 314 mButtonFrame.right -= mSubpixelSmoothingRight; 315 316 // frame: battery body area 317 mFrame.top += buttonHeight; 318 mFrame.left += mSubpixelSmoothingLeft; 319 mFrame.top += mSubpixelSmoothingLeft; 320 mFrame.right -= mSubpixelSmoothingRight; 321 mFrame.bottom -= mSubpixelSmoothingRight; 322 323 // set the battery charging color 324 mBatteryPaint.setColor(tracker.plugged ? mChargeColor : getColorForLevel(level)); 325 326 if (level >= FULL) { 327 drawFrac = 1f; 328 } else if (level <= mCriticalLevel) { 329 drawFrac = 0f; 330 } 331 332 final float levelTop = drawFrac == 1f ? mButtonFrame.top 333 : (mFrame.top + (mFrame.height() * (1f - drawFrac))); 334 335 // define the battery shape 336 mShapePath.reset(); 337 mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top); 338 mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top); 339 mShapePath.lineTo(mButtonFrame.right, mFrame.top); 340 mShapePath.lineTo(mFrame.right, mFrame.top); 341 mShapePath.lineTo(mFrame.right, mFrame.bottom); 342 mShapePath.lineTo(mFrame.left, mFrame.bottom); 343 mShapePath.lineTo(mFrame.left, mFrame.top); 344 mShapePath.lineTo(mButtonFrame.left, mFrame.top); 345 mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top); 346 347 if (tracker.plugged) { 348 // define the bolt shape 349 final float bl = mFrame.left + mFrame.width() / 4.5f; 350 final float bt = mFrame.top + mFrame.height() / 6f; 351 final float br = mFrame.right - mFrame.width() / 7f; 352 final float bb = mFrame.bottom - mFrame.height() / 10f; 353 if (mBoltFrame.left != bl || mBoltFrame.top != bt 354 || mBoltFrame.right != br || mBoltFrame.bottom != bb) { 355 mBoltFrame.set(bl, bt, br, bb); 356 mBoltPath.reset(); 357 mBoltPath.moveTo( 358 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), 359 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); 360 for (int i = 2; i < mBoltPoints.length; i += 2) { 361 mBoltPath.lineTo( 362 mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(), 363 mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height()); 364 } 365 mBoltPath.lineTo( 366 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), 367 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); 368 } 369 370 float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top); 371 boltPct = Math.min(Math.max(boltPct, 0), 1); 372 if (boltPct <= BOLT_LEVEL_THRESHOLD) { 373 // draw the bolt if opaque 374 c.drawPath(mBoltPath, mBoltPaint); 375 } else { 376 // otherwise cut the bolt out of the overall shape 377 mShapePath.op(mBoltPath, Path.Op.DIFFERENCE); 378 } 379 } 380 381 // compute percentage text 382 boolean pctOpaque = false; 383 float pctX = 0, pctY = 0; 384 String pctText = null; 385 if (!tracker.plugged && level > mCriticalLevel && mShowPercent) { 386 mTextPaint.setColor(getColorForLevel(level)); 387 mTextPaint.setTextSize(height * 388 (SINGLE_DIGIT_PERCENT ? 0.75f 389 : (tracker.level == 100 ? 0.38f : 0.5f))); 390 mTextHeight = -mTextPaint.getFontMetrics().ascent; 391 pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); 392 pctX = mWidth * 0.5f; 393 pctY = (mHeight + mTextHeight) * 0.47f; 394 pctOpaque = levelTop > pctY; 395 if (!pctOpaque) { 396 mTextPath.reset(); 397 mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath); 398 // cut the percentage text out of the overall shape 399 mShapePath.op(mTextPath, Path.Op.DIFFERENCE); 400 } 401 } 402 403 // draw the battery shape background 404 c.drawPath(mShapePath, mFramePaint); 405 406 // draw the battery shape, clipped to charging level 407 mFrame.top = levelTop; 408 mClipPath.reset(); 409 mClipPath.addRect(mFrame, Path.Direction.CCW); 410 mShapePath.op(mClipPath, Path.Op.INTERSECT); 411 c.drawPath(mShapePath, mBatteryPaint); 412 413 if (!tracker.plugged) { 414 if (level <= mCriticalLevel) { 415 // draw the warning text 416 final float x = mWidth * 0.5f; 417 final float y = (mHeight + mWarningTextHeight) * 0.48f; 418 c.drawText(mWarningString, x, y, mWarningTextPaint); 419 } else if (pctOpaque) { 420 // draw the percentage text 421 c.drawText(pctText, pctX, pctY, mTextPaint); 422 } 423 } 424 } 425 426 @Override 427 public boolean hasOverlappingRendering() { 428 return false; 429 } 430 431 private boolean mDemoMode; 432 private BatteryTracker mDemoTracker = new BatteryTracker(); 433 434 @Override 435 public void dispatchDemoCommand(String command, Bundle args) { 436 if (!mDemoMode && command.equals(COMMAND_ENTER)) { 437 mDemoMode = true; 438 mDemoTracker.level = mTracker.level; 439 mDemoTracker.plugged = mTracker.plugged; 440 } else if (mDemoMode && command.equals(COMMAND_EXIT)) { 441 mDemoMode = false; 442 postInvalidate(); 443 } else if (mDemoMode && command.equals(COMMAND_BATTERY)) { 444 String level = args.getString("level"); 445 String plugged = args.getString("plugged"); 446 if (level != null) { 447 mDemoTracker.level = Math.min(Math.max(Integer.parseInt(level), 0), 100); 448 } 449 if (plugged != null) { 450 mDemoTracker.plugged = Boolean.parseBoolean(plugged); 451 } 452 postInvalidate(); 453 } 454 } 455 456 private final class BatteryTracker extends BroadcastReceiver { 457 public static final int UNKNOWN_LEVEL = -1; 458 459 // current battery status 460 int level = UNKNOWN_LEVEL; 461 String percentStr; 462 int plugType; 463 boolean plugged; 464 int health; 465 int status; 466 String technology; 467 int voltage; 468 int temperature; 469 boolean testmode = false; 470 471 @Override 472 public void onReceive(Context context, Intent intent) { 473 final String action = intent.getAction(); 474 if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { 475 if (testmode && ! intent.getBooleanExtra("testmode", false)) return; 476 477 level = (int)(100f 478 * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) 479 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); 480 481 plugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); 482 plugged = plugType != 0; 483 health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 484 BatteryManager.BATTERY_HEALTH_UNKNOWN); 485 status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 486 BatteryManager.BATTERY_STATUS_UNKNOWN); 487 technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY); 488 voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); 489 temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); 490 491 setContentDescription( 492 context.getString(R.string.accessibility_battery_level, level)); 493 postInvalidate(); 494 } else if (action.equals(ACTION_LEVEL_TEST)) { 495 testmode = true; 496 post(new Runnable() { 497 int curLevel = 0; 498 int incr = 1; 499 int saveLevel = level; 500 int savePlugged = plugType; 501 Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED); 502 @Override 503 public void run() { 504 if (curLevel < 0) { 505 testmode = false; 506 dummy.putExtra("level", saveLevel); 507 dummy.putExtra("plugged", savePlugged); 508 dummy.putExtra("testmode", false); 509 } else { 510 dummy.putExtra("level", curLevel); 511 dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC 512 : 0); 513 dummy.putExtra("testmode", true); 514 } 515 getContext().sendBroadcast(dummy); 516 517 if (!testmode) return; 518 519 curLevel += incr; 520 if (curLevel == 100) { 521 incr *= -1; 522 } 523 postDelayed(this, 200); 524 } 525 }); 526 } 527 } 528 } 529 530 private final class SettingObserver extends ContentObserver { 531 public SettingObserver() { 532 super(new Handler()); 533 } 534 535 @Override 536 public void onChange(boolean selfChange, Uri uri) { 537 super.onChange(selfChange, uri); 538 updateShowPercent(); 539 postInvalidate(); 540 } 541 } 542 543 } 544