Files
frameworks_base/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
John Spurlock 3332ba54ae Heads-up notifications for low battery warnings.
Falls back to dialogs when heads-up are N/A.

Add new ongoing notification if battery saver mode
is active.  Offer to start battery saver on warnings,
if not already started.

True up BatteryMeterView's levels to the latest threshold
levels.  15% for first warning.

Bug:13329308
Change-Id: Id8ad11a1997079ee7165ae003a8fa1c744462ab3
2014-06-11 19:38:00 -04:00

430 lines
17 KiB
Java
Executable File

/*
* 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.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.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.BatteryManager;
import android.os.Bundle;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.View;
public class BatteryMeterView extends View implements DemoMode {
public static final String TAG = BatteryMeterView.class.getSimpleName();
public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST";
private static final boolean ENABLE_PERCENT = true;
private static final boolean SINGLE_DIGIT_PERCENT = false;
private static final boolean SHOW_100_PERCENT = false;
private static final int FULL = 96;
private static final float SUBPIXEL = 0.4f; // inset rects for softer edges
private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction
private final int[] mColors;
boolean mShowPercent = true;
private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint;
private float mTextHeight, mWarningTextHeight;
private int mHeight;
private int mWidth;
private String mWarningString;
private final int mCriticalLevel;
private final 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 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);
}
});
}
}
}
BatteryTracker mTracker = new BatteryTracker();
@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);
}
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
getContext().unregisterReceiver(mTracker);
}
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,
res.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();
mShowPercent = ENABLE_PERCENT && 0 != Settings.System.getInt(
context.getContentResolver(), "status_bar_show_battery_percent", 0);
mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol);
mCriticalLevel = mContext.getResources().getInteger(
com.android.internal.R.integer.config_criticalBatteryWarningLevel);
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 = getResources().getColor(R.color.batterymeter_charge_color);
mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBoltPaint.setColor(res.getColor(R.color.batterymeter_bolt_color));
mBoltPoints = loadBoltPoints(res);
}
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 int getColorForLevel(int percent) {
int thresh, color = 0;
for (int i=0; i<mColors.length; i+=2) {
thresh = mColors[i];
color = mColors[i+1];
if (percent <= thresh) return color;
}
return color;
}
@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 * 0.12f);
mFrame.set(0, 0, width, height);
mFrame.offset(pl, pt);
// button-frame: area above the battery body
mButtonFrame.set(
mFrame.left + width * 0.25f,
mFrame.top,
mFrame.right - width * 0.25f,
mFrame.top + buttonHeight);
mButtonFrame.top += SUBPIXEL;
mButtonFrame.left += SUBPIXEL;
mButtonFrame.right -= SUBPIXEL;
// frame: battery body area
mFrame.top += buttonHeight;
mFrame.left += SUBPIXEL;
mFrame.top += SUBPIXEL;
mFrame.right -= SUBPIXEL;
mFrame.bottom -= SUBPIXEL;
// 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
&& !(tracker.level == 100 && !SHOW_100_PERCENT)) {
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);
}
}
}
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();
}
}
}