/* * 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(); } } }