/* * Copyright (C) 2015 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.annotation.Nullable; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.ThemeConfig; import android.content.res.TypedArray; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.provider.Settings; import android.util.Log; import android.view.Gravity; import android.view.View; import com.android.systemui.statusbar.policy.BatteryController; import cyanogenmod.providers.CMSettings; import org.cyanogenmod.graphics.drawable.StopMotionVectorDrawable; public class BatteryMeterDrawable extends Drawable implements BatteryController.BatteryStateChangeCallback { private static final float ASPECT_RATIO = 9.5f / 14.5f; public static final String TAG = BatteryMeterDrawable.class.getSimpleName(); 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 // Values for the different battery styles public static final int BATTERY_STYLE_PORTRAIT = 0; public static final int BATTERY_STYLE_CIRCLE = 2; public static final int BATTERY_STYLE_HIDDEN = 4; public static final int BATTERY_STYLE_LANDSCAPE = 5; public static final int BATTERY_STYLE_TEXT = 6; private final int[] mColors; private final int mIntrinsicWidth; private final int mIntrinsicHeight; private boolean mShowPercent; private float mButtonHeightFraction; private float mSubpixelSmoothingLeft; private float mSubpixelSmoothingRight; private float mTextHeight, mWarningTextHeight; private int mIconTint = Color.WHITE; private float mOldDarkIntensity = 0f; private int mHeight; private int mWidth; private String mWarningString; private final int mCriticalLevel; private int mChargeColor; private final Path mBoltPath = new Path(); private final Path mPlusPath = new Path(); private final RectF mFrame = new RectF(); private final RectF mButtonFrame = new RectF(); private final RectF mBoltFrame = new RectF(); private final RectF mPlusFrame = 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 final SettingObserver mSettingObserver = new SettingObserver(); private final Context mContext; private final Handler mHandler; private int mLevel = -1; private boolean mPluggedIn; private boolean mListening; private boolean mIsAnimating; // stores charge-animation status to remove callbacks private float mTextX, mTextY; // precalculated position for drawText() to appear centered private boolean mInitialized; private Paint mTextAndBoltPaint; private Paint mWarningTextPaint; private Paint mClearPaint; private LayerDrawable mBatteryDrawable; private Drawable mFrameDrawable; private StopMotionVectorDrawable mLevelDrawable; private Drawable mBoltDrawable; private int mTextGravity; private int mCurrentBackgroundColor = 0; private int mCurrentFillColor = 0; public BatteryMeterDrawable(Context context, Handler handler, int frameColor) { // Portrait is the default drawable style this(context, handler, frameColor, BATTERY_STYLE_PORTRAIT); } public BatteryMeterDrawable(Context context, Handler handler, int frameColor, int style) { mContext = context; mHandler = handler; final Resources res = context.getResources(); 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(); 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); loadBatteryDrawables(res, style); // Load text gravity and blend mode final int[] attrs = new int[] { android.R.attr.gravity, R.attr.blendMode }; final int resId = getBatteryDrawableStyleResourceForStyle(style); PorterDuff.Mode xferMode = PorterDuff.Mode.XOR; if (resId != 0) { TypedArray a = mContext.obtainStyledAttributes(resId, attrs); mTextGravity = a.getInt(0, Gravity.CENTER); xferMode = PorterDuff.intToMode(a.getInt(1, PorterDuff.modeToInt(PorterDuff.Mode.XOR))); a.recycle(); } else { mTextGravity = Gravity.CENTER; } mTextAndBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD); mTextAndBoltPaint.setTypeface(font); mTextAndBoltPaint.setTextAlign(getPaintAlignmentFromGravity(mTextGravity)); mTextAndBoltPaint.setXfermode(new PorterDuffXfermode(xferMode)); mTextAndBoltPaint.setColor(mCurrentFillColor != 0 ? mCurrentFillColor : res.getColor(R.color.batterymeter_bolt_color)); mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mWarningTextPaint.setColor(mColors[1]); font = Typeface.create("sans-serif", Typeface.BOLD); mWarningTextPaint.setTypeface(font); mWarningTextPaint.setTextAlign(getPaintAlignmentFromGravity(mTextGravity)); mClearPaint = new Paint(); mClearPaint.setColor(0); 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); mIntrinsicWidth = context.getResources().getDimensionPixelSize(R.dimen.battery_width); mIntrinsicHeight = context.getResources().getDimensionPixelSize(R.dimen.battery_height); } @Override public int getIntrinsicHeight() { return mIntrinsicHeight; } @Override public int getIntrinsicWidth() { return mIntrinsicWidth; } public void startListening() { mListening = true; mContext.getContentResolver().registerContentObserver( CMSettings.System.getUriFor(CMSettings.System.STATUS_BAR_SHOW_BATTERY_PERCENT), false, mSettingObserver); updateShowPercent(); mBatteryController.addStateChangedCallback(this); } public void stopListening() { mListening = false; mContext.getContentResolver().unregisterContentObserver(mSettingObserver); mBatteryController.removeStateChangedCallback(this); } public void disableShowPercent() { mShowPercent = false; postInvalidate(); } private void postInvalidate() { mHandler.post(new Runnable() { @Override public void run() { invalidateSelf(); } }); } public void setBatteryController(BatteryController batteryController) { mBatteryController = batteryController; mPowerSaveEnabled = mBatteryController.isPowerSave(); } @Override public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { mLevel = level; mPluggedIn = pluggedIn; postInvalidate(); } @Override public void onPowerSaveChanged(boolean isPowerSave) { mPowerSaveEnabled = isPowerSave; invalidateSelf(); } 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; } private static float[] loadPlusPoints(Resources res) { final int[] pts = res.getIntArray(R.array.batterymeter_plus_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 public void setBounds(int left, int top, int right, int bottom) { super.setBounds(left, top, right, bottom); mHeight = bottom - top; mWidth = right - left; mWarningTextPaint.setTextSize(mHeight * 0.75f); mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent; } private void updateShowPercent() { mShowPercent = CMSettings.System.getInt(mContext.getContentResolver(), CMSettings.System.STATUS_BAR_SHOW_BATTERY_PERCENT, 0) == 1; } 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 = 0; int 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) { if (darkIntensity == mOldDarkIntensity) { return; } mCurrentBackgroundColor = getBackgroundColor(darkIntensity); mCurrentFillColor = getFillColor(darkIntensity); mIconTint = mCurrentFillColor; // Make bolt fully opaque for increased visibility mBoltDrawable.setTint(0xff000000 | mCurrentFillColor); mFrameDrawable.setTint(mCurrentBackgroundColor); updateBoltDrawableLayer(mBatteryDrawable, mBoltDrawable); invalidateSelf(); mOldDarkIntensity = darkIntensity; } 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) { if (!mInitialized) { init(); } drawBattery(c); } // Some stuff required by Drawable. @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { } @Override public int getOpacity() { return 0; } 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(); } } private void loadBatteryDrawables(Resources res, int style) { if (isThemeApplied()) { try { checkBatteryMeterDrawableValid(res, style); } catch (BatteryMeterDrawableException e) { Log.w(TAG, "Invalid themed battery meter drawable, falling back to system", e); /* Disable until the theme engine is brought up PackageManager pm = mContext.getPackageManager(); try { res = pm.getThemedResourcesForApplication(mContext.getPackageName(), ThemeConfig.SYSTEM_DEFAULT); } catch (PackageManager.NameNotFoundException nnfe) { // Ignore; this should not happen } */ } } final int drawableResId = getBatteryDrawableResourceForStyle(style); mBatteryDrawable = (LayerDrawable) res.getDrawable(drawableResId); mFrameDrawable = mBatteryDrawable.findDrawableByLayerId(R.id.battery_frame); mFrameDrawable.setTint(mCurrentBackgroundColor != 0 ? mCurrentBackgroundColor : res.getColor(R.color.batterymeter_frame_color)); // Set the animated vector drawable we will be stop-animating final Drawable levelDrawable = mBatteryDrawable.findDrawableByLayerId(R.id.battery_fill); mLevelDrawable = new StopMotionVectorDrawable(levelDrawable); mBoltDrawable = mBatteryDrawable.findDrawableByLayerId(R.id.battery_charge_indicator); } private boolean isThemeApplied() { final ThemeConfig themeConfig = ThemeConfig.getBootTheme(mContext.getContentResolver()); return themeConfig != null && !ThemeConfig.SYSTEM_DEFAULT.equals(themeConfig.getOverlayForStatusBar()); } private void checkBatteryMeterDrawableValid(Resources res, int style) { final int resId = getBatteryDrawableResourceForStyle(style); final Drawable batteryDrawable; try { batteryDrawable = res.getDrawable(resId); } catch (Resources.NotFoundException e) { throw new BatteryMeterDrawableException(res.getResourceName(resId) + " is an " + "invalid drawable", e); } // Check that the drawable is a LayerDrawable if (!(batteryDrawable instanceof LayerDrawable)) { throw new BatteryMeterDrawableException("Expected a LayerDrawable but received a " + batteryDrawable.getClass().getSimpleName()); } final LayerDrawable layerDrawable = (LayerDrawable) batteryDrawable; final Drawable frame = layerDrawable.findDrawableByLayerId(R.id.battery_frame); final Drawable level = layerDrawable.findDrawableByLayerId(R.id.battery_fill); final Drawable bolt = layerDrawable.findDrawableByLayerId(R.id.battery_charge_indicator); // Now, check that the required layers exist and are of the correct type if (frame == null) { throw new BatteryMeterDrawableException("Missing battery_frame drawble"); } if (bolt == null) { throw new BatteryMeterDrawableException( "Missing battery_charge_indicator drawable"); } if (level != null) { // Check that the level drawable is an AnimatedVectorDrawable if (!(level instanceof AnimatedVectorDrawable)) { throw new BatteryMeterDrawableException("Expected a AnimatedVectorDrawable " + "but received a " + level.getClass().getSimpleName()); } // Make sure we can stop-motion animate the level drawable try { StopMotionVectorDrawable smvd = new StopMotionVectorDrawable(level); smvd.setCurrentFraction(0.5f); } catch (Exception e) { throw new BatteryMeterDrawableException("Unable to perform stop motion on " + "battery_fill drawable", e); } } else { throw new BatteryMeterDrawableException("Missing battery_fill drawable"); } } private int getBatteryDrawableResourceForStyle(final int style) { switch (style) { case BATTERY_STYLE_LANDSCAPE: return R.drawable.ic_battery_landscape; case BATTERY_STYLE_CIRCLE: return R.drawable.ic_battery_circle; case BATTERY_STYLE_PORTRAIT: return R.drawable.ic_battery_portrait; default: return 0; } } private int getBatteryDrawableStyleResourceForStyle(final int style) { switch (style) { case BATTERY_STYLE_LANDSCAPE: return R.style.BatteryMeterViewDrawable_Landscape; case BATTERY_STYLE_CIRCLE: return R.style.BatteryMeterViewDrawable_Circle; case BATTERY_STYLE_PORTRAIT: return R.style.BatteryMeterViewDrawable_Portrait; default: return R.style.BatteryMeterViewDrawable; } } /** * Initializes all size dependent variables */ private void init() { // Not much we can do with zero width or height, we'll get another pass later if (mWidth <= 0 || mHeight <= 0) return; final float widthDiv2 = mWidth / 2f; // text size is width / 2 - 2dp for wiggle room final float textSize = widthDiv2 - mContext.getResources().getDisplayMetrics().density * 2; mTextAndBoltPaint.setTextSize(textSize); mWarningTextPaint.setTextSize(textSize); Rect iconBounds = new Rect(0, 0, mWidth, mHeight); mBatteryDrawable.setBounds(iconBounds); // Calculate text position Rect bounds = new Rect(); mTextAndBoltPaint.getTextBounds("99", 0, "99".length(), bounds); final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; // Compute mTextX based on text gravity if ((mTextGravity & Gravity.START) == Gravity.START) { mTextX = isRtl ? mWidth : 0; } else if ((mTextGravity & Gravity.END) == Gravity.END) { mTextX = isRtl ? 0 : mWidth; } else if ((mTextGravity & Gravity.LEFT) == Gravity.LEFT) { mTextX = 0; } else if ((mTextGravity & Gravity.RIGHT) == Gravity.RIGHT) { mTextX = mWidth; } else { mTextX = widthDiv2; } // Compute mTextY based on text gravity if ((mTextGravity & Gravity.TOP) == Gravity.TOP) { mTextY = bounds.height(); } else if ((mTextGravity & Gravity.BOTTOM) == Gravity.BOTTOM) { mTextY = mHeight; } else { mTextY = widthDiv2 + bounds.height() / 2.0f; } updateBoltDrawableLayer(mBatteryDrawable, mBoltDrawable); mInitialized = true; } // Creates a BitmapDrawable of the bolt so we can make use of // the XOR xfer mode with vector-based drawables private void updateBoltDrawableLayer(LayerDrawable batteryDrawable, Drawable boltDrawable) { BitmapDrawable newBoltDrawable; if (boltDrawable instanceof BitmapDrawable) { newBoltDrawable = (BitmapDrawable) boltDrawable.mutate(); } else { Bitmap boltBitmap = createBoltBitmap(boltDrawable); if (boltBitmap == null) { // Not much to do with a null bitmap so keep original bolt for now return; } Rect bounds = boltDrawable.getBounds(); newBoltDrawable = new BitmapDrawable(mContext.getResources(), boltBitmap); newBoltDrawable.setBounds(bounds); } newBoltDrawable.getPaint().set(mTextAndBoltPaint); batteryDrawable.setDrawableByLayerId(R.id.battery_charge_indicator, newBoltDrawable); } private Bitmap createBoltBitmap(Drawable boltDrawable) { // Not much we can do with zero width or height, we'll get another pass later if (mWidth <= 0 || mHeight <= 0) return null; Bitmap bolt; if (!(boltDrawable instanceof BitmapDrawable)) { Rect iconBounds = new Rect(0, 0, mWidth, mHeight); bolt = Bitmap.createBitmap(iconBounds.width(), iconBounds.height(), Bitmap.Config.ARGB_8888); if (bolt != null) { Canvas c = new Canvas(bolt); c.drawColor(-1, PorterDuff.Mode.CLEAR); boltDrawable.draw(c); } } else { bolt = ((BitmapDrawable) boltDrawable).getBitmap(); } return bolt; } private void drawBattery(Canvas canvas) { final int level = mLevel; mTextAndBoltPaint.setColor(getColorForLevel(level)); // Make sure we don't draw the charge indicator if not plugged in final Drawable d = mBatteryDrawable.findDrawableByLayerId(R.id.battery_charge_indicator); if (d instanceof BitmapDrawable) { // In case we are using a BitmapDrawable, which we should be unless something bad // happened, we need to change the paint rather than the alpha in case the blendMode // has been set to clear. Clear always clears regardless of alpha level ;) final BitmapDrawable bd = (BitmapDrawable) d; bd.getPaint().set(mPluggedIn ? mTextAndBoltPaint : mClearPaint); } else { d.setAlpha(mPluggedIn ? 255 : 0); } // Now draw the level indicator // Set the level and tint color of the fill drawable mLevelDrawable.setCurrentFraction(level / 100f); mLevelDrawable.setTint(getColorForLevel(level)); mBatteryDrawable.draw(canvas); // If chosen by options, draw percentage text in the middle // Always skip percentage when 100, so layout doesnt break if (!mPluggedIn) { drawPercentageText(canvas); } } private void drawPercentageText(Canvas canvas) { final int level = mLevel; if (level > mCriticalLevel && mShowPercent && level != 100) { // Draw the percentage text String pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level / 10) : level); mTextAndBoltPaint.setColor(getColorForLevel(level)); canvas.drawText(pctText, mTextX, mTextY, mTextAndBoltPaint); } else if (level <= mCriticalLevel) { // Draw the warning text canvas.drawText(mWarningString, mTextX, mTextY, mWarningTextPaint); } } private Paint.Align getPaintAlignmentFromGravity(int gravity) { final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; if ((gravity & Gravity.START) == Gravity.START) { return isRtl ? Paint.Align.RIGHT : Paint.Align.LEFT; } if ((gravity & Gravity.END) == Gravity.END) { return isRtl ? Paint.Align.LEFT : Paint.Align.RIGHT; } if ((gravity & Gravity.LEFT) == Gravity.LEFT) return Paint.Align.LEFT; if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) return Paint.Align.RIGHT; // Default to center return Paint.Align.CENTER; } private class BatteryMeterDrawableException extends RuntimeException { public BatteryMeterDrawableException(String detailMessage) { super(detailMessage); } public BatteryMeterDrawableException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } } }