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