/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui;
import android.animation.ArgbEvaluator;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.ContentObserver;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.View;
import com.android.systemui.statusbar.policy.BatteryController;
public class BatteryMeterView extends View implements DemoMode,
BatteryController.BatteryStateChangeCallback {
public static final String TAG = BatteryMeterView.class.getSimpleName();
public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST";
public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent";
private static final boolean SINGLE_DIGIT_PERCENT = false;
private static final int FULL = 96;
private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction
private final int[] mColors;
private boolean mShowPercent;
private float mButtonHeightFraction;
private float mSubpixelSmoothingLeft;
private float mSubpixelSmoothingRight;
private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint;
private float mTextHeight, mWarningTextHeight;
private int mIconTint = Color.WHITE;
private int mHeight;
private int mWidth;
private String mWarningString;
private final int mCriticalLevel;
private int mChargeColor;
private final float[] mBoltPoints;
private final Path mBoltPath = new Path();
private final RectF mFrame = new RectF();
private final RectF mButtonFrame = new RectF();
private final RectF mBoltFrame = new RectF();
private final Path mShapePath = new Path();
private final Path mClipPath = new Path();
private final Path mTextPath = new Path();
private BatteryController mBatteryController;
private boolean mPowerSaveEnabled;
private int mDarkModeBackgroundColor;
private int mDarkModeFillColor;
private int mLightModeBackgroundColor;
private int mLightModeFillColor;
private BatteryTracker mTracker = new BatteryTracker();
private final SettingObserver mSettingObserver = new SettingObserver();
public BatteryMeterView(Context context) {
this(context, null, 0);
}
public BatteryMeterView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final Resources res = context.getResources();
TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView,
defStyle, 0);
final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
context.getColor(R.color.batterymeter_frame_color));
TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels);
TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values);
final int N = levels.length();
mColors = new int[2*N];
for (int i=0; i<N; i++) {
mColors[2*i] = levels.getInt(i, 0);
mColors[2*i+1] = colors.getColor(i, 0);
}
levels.recycle();
colors.recycle();
atts.recycle();
updateShowPercent();
mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol);
mCriticalLevel = mContext.getResources().getInteger(
com.android.internal.R.integer.config_criticalBatteryWarningLevel);
mButtonHeightFraction = context.getResources().getFraction(
R.fraction.battery_button_height_fraction, 1, 1);
mSubpixelSmoothingLeft = context.getResources().getFraction(
R.fraction.battery_subpixel_smoothing_left, 1, 1);
mSubpixelSmoothingRight = context.getResources().getFraction(
R.fraction.battery_subpixel_smoothing_right, 1, 1);
mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFramePaint.setColor(frameColor);
mFramePaint.setDither(true);
mFramePaint.setStrokeWidth(0);
mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE);
mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBatteryPaint.setDither(true);
mBatteryPaint.setStrokeWidth(0);
mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD);
mTextPaint.setTypeface(font);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mWarningTextPaint.setColor(mColors[1]);
font = Typeface.create("sans-serif", Typeface.BOLD);
mWarningTextPaint.setTypeface(font);
mWarningTextPaint.setTextAlign(Paint.Align.CENTER);
mChargeColor = context.getColor(R.color.batterymeter_charge_color);
mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBoltPaint.setColor(context.getColor(R.color.batterymeter_bolt_color));
mBoltPoints = loadBoltPoints(res);
mDarkModeBackgroundColor =
context.getColor(R.color.dark_mode_icon_color_dual_tone_background);
mDarkModeFillColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_fill);
mLightModeBackgroundColor =
context.getColor(R.color.light_mode_icon_color_dual_tone_background);
mLightModeFillColor = context.getColor(R.color.light_mode_icon_color_dual_tone_fill);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
filter.addAction(ACTION_LEVEL_TEST);
final Intent sticky = getContext().registerReceiver(mTracker, filter);
if (sticky != null) {
// preload the battery level
mTracker.onReceive(getContext(), sticky);
}
mBatteryController.addStateChangedCallback(this);
getContext().getContentResolver().registerContentObserver(
Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
getContext().unregisterReceiver(mTracker);
mBatteryController.removeStateChangedCallback(this);
getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
}
public void setBatteryController(BatteryController batteryController) {
mBatteryController = batteryController;
mPowerSaveEnabled = mBatteryController.isPowerSave();
}
@Override
public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
// TODO: Use this callback instead of own broadcast receiver.
}
@Override
public void onPowerSaveChanged() {
mPowerSaveEnabled = mBatteryController.isPowerSave();
invalidate();
}
private static float[] loadBoltPoints(Resources res) {
final int[] pts = res.getIntArray(R.array.batterymeter_bolt_points);
int maxX = 0, maxY = 0;
for (int i = 0; i < pts.length; i += 2) {
maxX = Math.max(maxX, pts[i]);
maxY = Math.max(maxY, pts[i + 1]);
}
final float[] ptsF = new float[pts.length];
for (int i = 0; i < pts.length; i += 2) {
ptsF[i] = (float)pts[i] / maxX;
ptsF[i + 1] = (float)pts[i + 1] / maxY;
}
return ptsF;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mHeight = h;
mWidth = w;
mWarningTextPaint.setTextSize(h * 0.75f);
mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent;
}
private void updateShowPercent() {
mShowPercent = 0 != Settings.System.getInt(getContext().getContentResolver(),
SHOW_PERCENT_SETTING, 0);
}
private int getColorForLevel(int percent) {
// If we are in power save mode, always use the normal color.
if (mPowerSaveEnabled) {
return mColors[mColors.length-1];
}
int thresh, color = 0;
for (int i=0; i<mColors.length; i+=2) {
thresh = mColors[i];
color = mColors[i+1];
if (percent <= thresh) {
// Respect tinting for "normal" level
if (i == mColors.length-2) {
return mIconTint;
} else {
return color;
}
}
}
return color;
}
public void setDarkIntensity(float darkIntensity) {
int backgroundColor = getBackgroundColor(darkIntensity);
int fillColor = getFillColor(darkIntensity);
mIconTint = fillColor;
mFramePaint.setColor(backgroundColor);
mBoltPaint.setColor(fillColor);
mChargeColor = fillColor;
invalidate();
}
private int getBackgroundColor(float darkIntensity) {
return getColorForDarkIntensity(
darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
}
private int getFillColor(float darkIntensity) {
return getColorForDarkIntensity(
darkIntensity, mLightModeFillColor, mDarkModeFillColor);
}
private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
}
@Override
public void draw(Canvas c) {
BatteryTracker tracker = mDemoMode ? mDemoTracker : mTracker;
final int level = tracker.level;
if (level == BatteryTracker.UNKNOWN_LEVEL) return;
float drawFrac = (float) level / 100f;
final int pt = getPaddingTop();
final int pl = getPaddingLeft();
final int pr = getPaddingRight();
final int pb = getPaddingBottom();
final int height = mHeight - pt - pb;
final int width = mWidth - pl - pr;
final int buttonHeight = (int) (height * mButtonHeightFraction);
mFrame.set(0, 0, width, height);
mFrame.offset(pl, pt);
// button-frame: area above the battery body
mButtonFrame.set(
mFrame.left + Math.round(width * 0.25f),
mFrame.top,
mFrame.right - Math.round(width * 0.25f),
mFrame.top + buttonHeight);
mButtonFrame.top += mSubpixelSmoothingLeft;
mButtonFrame.left += mSubpixelSmoothingLeft;
mButtonFrame.right -= mSubpixelSmoothingRight;
// frame: battery body area
mFrame.top += buttonHeight;
mFrame.left += mSubpixelSmoothingLeft;
mFrame.top += mSubpixelSmoothingLeft;
mFrame.right -= mSubpixelSmoothingRight;
mFrame.bottom -= mSubpixelSmoothingRight;
// set the battery charging color
mBatteryPaint.setColor(tracker.plugged ? mChargeColor : getColorForLevel(level));
if (level >= FULL) {
drawFrac = 1f;
} else if (level <= mCriticalLevel) {
drawFrac = 0f;
}
final float levelTop = drawFrac == 1f ? mButtonFrame.top
: (mFrame.top + (mFrame.height() * (1f - drawFrac)));
// define the battery shape
mShapePath.reset();
mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top);
mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top);
mShapePath.lineTo(mButtonFrame.right, mFrame.top);
mShapePath.lineTo(mFrame.right, mFrame.top);
mShapePath.lineTo(mFrame.right, mFrame.bottom);
mShapePath.lineTo(mFrame.left, mFrame.bottom);
mShapePath.lineTo(mFrame.left, mFrame.top);
mShapePath.lineTo(mButtonFrame.left, mFrame.top);
mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top);
if (tracker.plugged) {
// define the bolt shape
final float bl = mFrame.left + mFrame.width() / 4.5f;
final float bt = mFrame.top + mFrame.height() / 6f;
final float br = mFrame.right - mFrame.width() / 7f;
final float bb = mFrame.bottom - mFrame.height() / 10f;
if (mBoltFrame.left != bl || mBoltFrame.top != bt
|| mBoltFrame.right != br || mBoltFrame.bottom != bb) {
mBoltFrame.set(bl, bt, br, bb);
mBoltPath.reset();
mBoltPath.moveTo(
mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
for (int i = 2; i < mBoltPoints.length; i += 2) {
mBoltPath.lineTo(
mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(),
mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height());
}
mBoltPath.lineTo(
mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
}
float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top);
boltPct = Math.min(Math.max(boltPct, 0), 1);
if (boltPct <= BOLT_LEVEL_THRESHOLD) {
// draw the bolt if opaque
c.drawPath(mBoltPath, mBoltPaint);
} else {
// otherwise cut the bolt out of the overall shape
mShapePath.op(mBoltPath, Path.Op.DIFFERENCE);
}
}
// compute percentage text
boolean pctOpaque = false;
float pctX = 0, pctY = 0;
String pctText = null;
if (!tracker.plugged && level > mCriticalLevel && mShowPercent) {
mTextPaint.setColor(getColorForLevel(level));
mTextPaint.setTextSize(height *
(SINGLE_DIGIT_PERCENT ? 0.75f
: (tracker.level == 100 ? 0.38f : 0.5f)));
mTextHeight = -mTextPaint.getFontMetrics().ascent;
pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level);
pctX = mWidth * 0.5f;
pctY = (mHeight + mTextHeight) * 0.47f;
pctOpaque = levelTop > pctY;
if (!pctOpaque) {
mTextPath.reset();
mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath);
// cut the percentage text out of the overall shape
mShapePath.op(mTextPath, Path.Op.DIFFERENCE);
}
}
// draw the battery shape background
c.drawPath(mShapePath, mFramePaint);
// draw the battery shape, clipped to charging level
mFrame.top = levelTop;
mClipPath.reset();
mClipPath.addRect(mFrame, Path.Direction.CCW);
mShapePath.op(mClipPath, Path.Op.INTERSECT);
c.drawPath(mShapePath, mBatteryPaint);
if (!tracker.plugged) {
if (level <= mCriticalLevel) {
// draw the warning text
final float x = mWidth * 0.5f;
final float y = (mHeight + mWarningTextHeight) * 0.48f;
c.drawText(mWarningString, x, y, mWarningTextPaint);
} else if (pctOpaque) {
// draw the percentage text
c.drawText(pctText, pctX, pctY, mTextPaint);
}
}
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
private boolean mDemoMode;
private BatteryTracker mDemoTracker = new BatteryTracker();
@Override
public void dispatchDemoCommand(String command, Bundle args) {
if (!mDemoMode && command.equals(COMMAND_ENTER)) {
mDemoMode = true;
mDemoTracker.level = mTracker.level;
mDemoTracker.plugged = mTracker.plugged;
} else if (mDemoMode && command.equals(COMMAND_EXIT)) {
mDemoMode = false;
postInvalidate();
} else if (mDemoMode && command.equals(COMMAND_BATTERY)) {
String level = args.getString("level");
String plugged = args.getString("plugged");
if (level != null) {
mDemoTracker.level = Math.min(Math.max(Integer.parseInt(level), 0), 100);
}
if (plugged != null) {
mDemoTracker.plugged = Boolean.parseBoolean(plugged);
}
postInvalidate();
}
}
private final class BatteryTracker extends BroadcastReceiver {
public static final int UNKNOWN_LEVEL = -1;
// current battery status
int level = UNKNOWN_LEVEL;
String percentStr;
int plugType;
boolean plugged;
int health;
int status;
String technology;
int voltage;
int temperature;
boolean testmode = false;
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
if (testmode && ! intent.getBooleanExtra("testmode", false)) return;
level = (int)(100f
* intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
/ intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100));
plugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
plugged = plugType != 0;
health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH,
BatteryManager.BATTERY_HEALTH_UNKNOWN);
status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
BatteryManager.BATTERY_STATUS_UNKNOWN);
technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY);
voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0);
temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0);
setContentDescription(
context.getString(R.string.accessibility_battery_level, level));
postInvalidate();
} else if (action.equals(ACTION_LEVEL_TEST)) {
testmode = true;
post(new Runnable() {
int curLevel = 0;
int incr = 1;
int saveLevel = level;
int savePlugged = plugType;
Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED);
@Override
public void run() {
if (curLevel < 0) {
testmode = false;
dummy.putExtra("level", saveLevel);
dummy.putExtra("plugged", savePlugged);
dummy.putExtra("testmode", false);
} else {
dummy.putExtra("level", curLevel);
dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC
: 0);
dummy.putExtra("testmode", true);
}
getContext().sendBroadcast(dummy);
if (!testmode) return;
curLevel += incr;
if (curLevel == 100) {
incr *= -1;
}
postDelayed(this, 200);
}
});
}
}
}
private final class SettingObserver extends ContentObserver {
public SettingObserver() {
super(new Handler());
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
updateShowPercent();
postInvalidate();
}
}
}