Home | History | Annotate | Download | only in systemui
      1 /*
      2  * Copyright (C) 2015 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.animation.ArgbEvaluator;
     20 import android.annotation.Nullable;
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.content.res.TypedArray;
     24 import android.database.ContentObserver;
     25 import android.graphics.Canvas;
     26 import android.graphics.Color;
     27 import android.graphics.ColorFilter;
     28 import android.graphics.Paint;
     29 import android.graphics.Path;
     30 import android.graphics.RectF;
     31 import android.graphics.Typeface;
     32 import android.graphics.drawable.Drawable;
     33 import android.net.Uri;
     34 import android.os.Bundle;
     35 import android.os.Handler;
     36 import android.provider.Settings;
     37 
     38 import com.android.systemui.statusbar.policy.BatteryController;
     39 
     40 public class BatteryMeterDrawable extends Drawable implements
     41         BatteryController.BatteryStateChangeCallback {
     42 
     43     private static final float ASPECT_RATIO = 9.5f / 14.5f;
     44     public static final String TAG = BatteryMeterDrawable.class.getSimpleName();
     45     public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent";
     46 
     47     private static final boolean SINGLE_DIGIT_PERCENT = false;
     48 
     49     private static final int FULL = 96;
     50 
     51     private static final float BOLT_LEVEL_THRESHOLD = 0.3f;  // opaque bolt below this fraction
     52 
     53     private final int[] mColors;
     54     private final int mIntrinsicWidth;
     55     private final int mIntrinsicHeight;
     56 
     57     private boolean mShowPercent;
     58     private float mButtonHeightFraction;
     59     private float mSubpixelSmoothingLeft;
     60     private float mSubpixelSmoothingRight;
     61     private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint,
     62             mPlusPaint;
     63     private float mTextHeight, mWarningTextHeight;
     64     private int mIconTint = Color.WHITE;
     65     private float mOldDarkIntensity = 0f;
     66 
     67     private int mHeight;
     68     private int mWidth;
     69     private String mWarningString;
     70     private final int mCriticalLevel;
     71     private int mChargeColor;
     72     private final float[] mBoltPoints;
     73     private final Path mBoltPath = new Path();
     74     private final float[] mPlusPoints;
     75     private final Path mPlusPath = new Path();
     76 
     77     private final RectF mFrame = new RectF();
     78     private final RectF mButtonFrame = new RectF();
     79     private final RectF mBoltFrame = new RectF();
     80     private final RectF mPlusFrame = new RectF();
     81 
     82     private final Path mShapePath = new Path();
     83     private final Path mClipPath = new Path();
     84     private final Path mTextPath = new Path();
     85 
     86     private BatteryController mBatteryController;
     87     private boolean mPowerSaveEnabled;
     88 
     89     private int mDarkModeBackgroundColor;
     90     private int mDarkModeFillColor;
     91 
     92     private int mLightModeBackgroundColor;
     93     private int mLightModeFillColor;
     94 
     95     private final SettingObserver mSettingObserver = new SettingObserver();
     96 
     97     private final Context mContext;
     98     private final Handler mHandler;
     99 
    100     private int mLevel = -1;
    101     private boolean mPluggedIn;
    102     private boolean mListening;
    103 
    104     public BatteryMeterDrawable(Context context, Handler handler, int frameColor) {
    105         mContext = context;
    106         mHandler = handler;
    107         final Resources res = context.getResources();
    108         TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels);
    109         TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values);
    110 
    111         final int N = levels.length();
    112         mColors = new int[2*N];
    113         for (int i=0; i<N; i++) {
    114             mColors[2*i] = levels.getInt(i, 0);
    115             mColors[2*i+1] = colors.getColor(i, 0);
    116         }
    117         levels.recycle();
    118         colors.recycle();
    119         updateShowPercent();
    120         mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol);
    121         mCriticalLevel = mContext.getResources().getInteger(
    122                 com.android.internal.R.integer.config_criticalBatteryWarningLevel);
    123         mButtonHeightFraction = context.getResources().getFraction(
    124                 R.fraction.battery_button_height_fraction, 1, 1);
    125         mSubpixelSmoothingLeft = context.getResources().getFraction(
    126                 R.fraction.battery_subpixel_smoothing_left, 1, 1);
    127         mSubpixelSmoothingRight = context.getResources().getFraction(
    128                 R.fraction.battery_subpixel_smoothing_right, 1, 1);
    129 
    130         mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    131         mFramePaint.setColor(frameColor);
    132         mFramePaint.setDither(true);
    133         mFramePaint.setStrokeWidth(0);
    134         mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE);
    135 
    136         mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    137         mBatteryPaint.setDither(true);
    138         mBatteryPaint.setStrokeWidth(0);
    139         mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    140 
    141         mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    142         Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD);
    143         mTextPaint.setTypeface(font);
    144         mTextPaint.setTextAlign(Paint.Align.CENTER);
    145 
    146         mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    147         mWarningTextPaint.setColor(mColors[1]);
    148         font = Typeface.create("sans-serif", Typeface.BOLD);
    149         mWarningTextPaint.setTypeface(font);
    150         mWarningTextPaint.setTextAlign(Paint.Align.CENTER);
    151 
    152         mChargeColor = context.getColor(R.color.batterymeter_charge_color);
    153 
    154         mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    155         mBoltPaint.setColor(context.getColor(R.color.batterymeter_bolt_color));
    156         mBoltPoints = loadBoltPoints(res);
    157 
    158         mPlusPaint = new Paint(mBoltPaint);
    159         mPlusPoints = loadPlusPoints(res);
    160 
    161         mDarkModeBackgroundColor =
    162                 context.getColor(R.color.dark_mode_icon_color_dual_tone_background);
    163         mDarkModeFillColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_fill);
    164         mLightModeBackgroundColor =
    165                 context.getColor(R.color.light_mode_icon_color_dual_tone_background);
    166         mLightModeFillColor = context.getColor(R.color.light_mode_icon_color_dual_tone_fill);
    167 
    168         mIntrinsicWidth = context.getResources().getDimensionPixelSize(R.dimen.battery_width);
    169         mIntrinsicHeight = context.getResources().getDimensionPixelSize(R.dimen.battery_height);
    170     }
    171 
    172     @Override
    173     public int getIntrinsicHeight() {
    174         return mIntrinsicHeight;
    175     }
    176 
    177     @Override
    178     public int getIntrinsicWidth() {
    179         return mIntrinsicWidth;
    180     }
    181 
    182     public void startListening() {
    183         mListening = true;
    184         mContext.getContentResolver().registerContentObserver(
    185                 Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver);
    186         updateShowPercent();
    187         mBatteryController.addStateChangedCallback(this);
    188     }
    189 
    190     public void stopListening() {
    191         mListening = false;
    192         mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
    193         mBatteryController.removeStateChangedCallback(this);
    194     }
    195 
    196     public void disableShowPercent() {
    197         mShowPercent = false;
    198         postInvalidate();
    199     }
    200 
    201     private void postInvalidate() {
    202         mHandler.post(new Runnable() {
    203             @Override
    204             public void run() {
    205                 invalidateSelf();
    206             }
    207         });
    208     }
    209 
    210     public void setBatteryController(BatteryController batteryController) {
    211         mBatteryController = batteryController;
    212         mPowerSaveEnabled = mBatteryController.isPowerSave();
    213     }
    214 
    215     @Override
    216     public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
    217         mLevel = level;
    218         mPluggedIn = pluggedIn;
    219 
    220         postInvalidate();
    221     }
    222 
    223     @Override
    224     public void onPowerSaveChanged(boolean isPowerSave) {
    225         mPowerSaveEnabled = isPowerSave;
    226         invalidateSelf();
    227     }
    228 
    229     private static float[] loadBoltPoints(Resources res) {
    230         final int[] pts = res.getIntArray(R.array.batterymeter_bolt_points);
    231         int maxX = 0, maxY = 0;
    232         for (int i = 0; i < pts.length; i += 2) {
    233             maxX = Math.max(maxX, pts[i]);
    234             maxY = Math.max(maxY, pts[i + 1]);
    235         }
    236         final float[] ptsF = new float[pts.length];
    237         for (int i = 0; i < pts.length; i += 2) {
    238             ptsF[i] = (float)pts[i] / maxX;
    239             ptsF[i + 1] = (float)pts[i + 1] / maxY;
    240         }
    241         return ptsF;
    242     }
    243 
    244     private static float[] loadPlusPoints(Resources res) {
    245         final int[] pts = res.getIntArray(R.array.batterymeter_plus_points);
    246         int maxX = 0, maxY = 0;
    247         for (int i = 0; i < pts.length; i += 2) {
    248             maxX = Math.max(maxX, pts[i]);
    249             maxY = Math.max(maxY, pts[i + 1]);
    250         }
    251         final float[] ptsF = new float[pts.length];
    252         for (int i = 0; i < pts.length; i += 2) {
    253             ptsF[i] = (float)pts[i] / maxX;
    254             ptsF[i + 1] = (float)pts[i + 1] / maxY;
    255         }
    256         return ptsF;
    257     }
    258 
    259     @Override
    260     public void setBounds(int left, int top, int right, int bottom) {
    261         super.setBounds(left, top, right, bottom);
    262         mHeight = bottom - top;
    263         mWidth = right - left;
    264         mWarningTextPaint.setTextSize(mHeight * 0.75f);
    265         mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent;
    266     }
    267 
    268     private void updateShowPercent() {
    269         mShowPercent = 0 != Settings.System.getInt(mContext.getContentResolver(),
    270                 SHOW_PERCENT_SETTING, 0);
    271     }
    272 
    273     private int getColorForLevel(int percent) {
    274 
    275         // If we are in power save mode, always use the normal color.
    276         if (mPowerSaveEnabled) {
    277             return mColors[mColors.length-1];
    278         }
    279         int thresh, color = 0;
    280         for (int i=0; i<mColors.length; i+=2) {
    281             thresh = mColors[i];
    282             color = mColors[i+1];
    283             if (percent <= thresh) {
    284 
    285                 // Respect tinting for "normal" level
    286                 if (i == mColors.length-2) {
    287                     return mIconTint;
    288                 } else {
    289                     return color;
    290                 }
    291             }
    292         }
    293         return color;
    294     }
    295 
    296     public void setDarkIntensity(float darkIntensity) {
    297         if (darkIntensity == mOldDarkIntensity) {
    298             return;
    299         }
    300         int backgroundColor = getBackgroundColor(darkIntensity);
    301         int fillColor = getFillColor(darkIntensity);
    302         mIconTint = fillColor;
    303         mFramePaint.setColor(backgroundColor);
    304         mBoltPaint.setColor(fillColor);
    305         mChargeColor = fillColor;
    306         invalidateSelf();
    307         mOldDarkIntensity = darkIntensity;
    308     }
    309 
    310     private int getBackgroundColor(float darkIntensity) {
    311         return getColorForDarkIntensity(
    312                 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
    313     }
    314 
    315     private int getFillColor(float darkIntensity) {
    316         return getColorForDarkIntensity(
    317                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
    318     }
    319 
    320     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
    321         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
    322     }
    323 
    324     @Override
    325     public void draw(Canvas c) {
    326         final int level = mLevel;
    327 
    328         if (level == -1) return;
    329 
    330         float drawFrac = (float) level / 100f;
    331         final int height = mHeight;
    332         final int width = (int) (ASPECT_RATIO * mHeight);
    333         int px = (mWidth - width) / 2;
    334 
    335         final int buttonHeight = (int) (height * mButtonHeightFraction);
    336 
    337         mFrame.set(0, 0, width, height);
    338         mFrame.offset(px, 0);
    339 
    340         // button-frame: area above the battery body
    341         mButtonFrame.set(
    342                 mFrame.left + Math.round(width * 0.25f),
    343                 mFrame.top,
    344                 mFrame.right - Math.round(width * 0.25f),
    345                 mFrame.top + buttonHeight);
    346 
    347         mButtonFrame.top += mSubpixelSmoothingLeft;
    348         mButtonFrame.left += mSubpixelSmoothingLeft;
    349         mButtonFrame.right -= mSubpixelSmoothingRight;
    350 
    351         // frame: battery body area
    352         mFrame.top += buttonHeight;
    353         mFrame.left += mSubpixelSmoothingLeft;
    354         mFrame.top += mSubpixelSmoothingLeft;
    355         mFrame.right -= mSubpixelSmoothingRight;
    356         mFrame.bottom -= mSubpixelSmoothingRight;
    357 
    358         // set the battery charging color
    359         mBatteryPaint.setColor(mPluggedIn ? mChargeColor : getColorForLevel(level));
    360 
    361         if (level >= FULL) {
    362             drawFrac = 1f;
    363         } else if (level <= mCriticalLevel) {
    364             drawFrac = 0f;
    365         }
    366 
    367         final float levelTop = drawFrac == 1f ? mButtonFrame.top
    368                 : (mFrame.top + (mFrame.height() * (1f - drawFrac)));
    369 
    370         // define the battery shape
    371         mShapePath.reset();
    372         mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top);
    373         mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top);
    374         mShapePath.lineTo(mButtonFrame.right, mFrame.top);
    375         mShapePath.lineTo(mFrame.right, mFrame.top);
    376         mShapePath.lineTo(mFrame.right, mFrame.bottom);
    377         mShapePath.lineTo(mFrame.left, mFrame.bottom);
    378         mShapePath.lineTo(mFrame.left, mFrame.top);
    379         mShapePath.lineTo(mButtonFrame.left, mFrame.top);
    380         mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top);
    381 
    382         if (mPluggedIn) {
    383             // define the bolt shape
    384             final float bl = mFrame.left + mFrame.width() / 4f;
    385             final float bt = mFrame.top + mFrame.height() / 6f;
    386             final float br = mFrame.right - mFrame.width() / 4f;
    387             final float bb = mFrame.bottom - mFrame.height() / 10f;
    388             if (mBoltFrame.left != bl || mBoltFrame.top != bt
    389                     || mBoltFrame.right != br || mBoltFrame.bottom != bb) {
    390                 mBoltFrame.set(bl, bt, br, bb);
    391                 mBoltPath.reset();
    392                 mBoltPath.moveTo(
    393                         mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
    394                         mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
    395                 for (int i = 2; i < mBoltPoints.length; i += 2) {
    396                     mBoltPath.lineTo(
    397                             mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(),
    398                             mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height());
    399                 }
    400                 mBoltPath.lineTo(
    401                         mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
    402                         mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
    403             }
    404 
    405             float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top);
    406             boltPct = Math.min(Math.max(boltPct, 0), 1);
    407             if (boltPct <= BOLT_LEVEL_THRESHOLD) {
    408                 // draw the bolt if opaque
    409                 c.drawPath(mBoltPath, mBoltPaint);
    410             } else {
    411                 // otherwise cut the bolt out of the overall shape
    412                 mShapePath.op(mBoltPath, Path.Op.DIFFERENCE);
    413             }
    414         } else if (mPowerSaveEnabled) {
    415             // define the plus shape
    416             final float pw = mFrame.width() * 2 / 3;
    417             final float pl = mFrame.left + (mFrame.width() - pw) / 2;
    418             final float pt = mFrame.top + (mFrame.height() - pw) / 2;
    419             final float pr = mFrame.right - (mFrame.width() - pw) / 2;
    420             final float pb = mFrame.bottom - (mFrame.height() - pw) / 2;
    421             if (mPlusFrame.left != pl || mPlusFrame.top != pt
    422                     || mPlusFrame.right != pr || mPlusFrame.bottom != pb) {
    423                 mPlusFrame.set(pl, pt, pr, pb);
    424                 mPlusPath.reset();
    425                 mPlusPath.moveTo(
    426                         mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
    427                         mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
    428                 for (int i = 2; i < mPlusPoints.length; i += 2) {
    429                     mPlusPath.lineTo(
    430                             mPlusFrame.left + mPlusPoints[i] * mPlusFrame.width(),
    431                             mPlusFrame.top + mPlusPoints[i + 1] * mPlusFrame.height());
    432                 }
    433                 mPlusPath.lineTo(
    434                         mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
    435                         mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
    436             }
    437 
    438             float boltPct = (mPlusFrame.bottom - levelTop) / (mPlusFrame.bottom - mPlusFrame.top);
    439             boltPct = Math.min(Math.max(boltPct, 0), 1);
    440             if (boltPct <= BOLT_LEVEL_THRESHOLD) {
    441                 // draw the bolt if opaque
    442                 c.drawPath(mPlusPath, mPlusPaint);
    443             } else {
    444                 // otherwise cut the bolt out of the overall shape
    445                 mShapePath.op(mPlusPath, Path.Op.DIFFERENCE);
    446             }
    447         }
    448 
    449         // compute percentage text
    450         boolean pctOpaque = false;
    451         float pctX = 0, pctY = 0;
    452         String pctText = null;
    453         if (!mPluggedIn && !mPowerSaveEnabled && level > mCriticalLevel && mShowPercent) {
    454             mTextPaint.setColor(getColorForLevel(level));
    455             mTextPaint.setTextSize(height *
    456                     (SINGLE_DIGIT_PERCENT ? 0.75f
    457                             : (mLevel == 100 ? 0.38f : 0.5f)));
    458             mTextHeight = -mTextPaint.getFontMetrics().ascent;
    459             pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level);
    460             pctX = mWidth * 0.5f;
    461             pctY = (mHeight + mTextHeight) * 0.47f;
    462             pctOpaque = levelTop > pctY;
    463             if (!pctOpaque) {
    464                 mTextPath.reset();
    465                 mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath);
    466                 // cut the percentage text out of the overall shape
    467                 mShapePath.op(mTextPath, Path.Op.DIFFERENCE);
    468             }
    469         }
    470 
    471         // draw the battery shape background
    472         c.drawPath(mShapePath, mFramePaint);
    473 
    474         // draw the battery shape, clipped to charging level
    475         mFrame.top = levelTop;
    476         mClipPath.reset();
    477         mClipPath.addRect(mFrame,  Path.Direction.CCW);
    478         mShapePath.op(mClipPath, Path.Op.INTERSECT);
    479         c.drawPath(mShapePath, mBatteryPaint);
    480 
    481         if (!mPluggedIn && !mPowerSaveEnabled) {
    482             if (level <= mCriticalLevel) {
    483                 // draw the warning text
    484                 final float x = mWidth * 0.5f;
    485                 final float y = (mHeight + mWarningTextHeight) * 0.48f;
    486                 c.drawText(mWarningString, x, y, mWarningTextPaint);
    487             } else if (pctOpaque) {
    488                 // draw the percentage text
    489                 c.drawText(pctText, pctX, pctY, mTextPaint);
    490             }
    491         }
    492     }
    493 
    494     // Some stuff required by Drawable.
    495     @Override
    496     public void setAlpha(int alpha) {
    497     }
    498 
    499     @Override
    500     public void setColorFilter(@Nullable ColorFilter colorFilter) {
    501     }
    502 
    503     @Override
    504     public int getOpacity() {
    505         return 0;
    506     }
    507 
    508     private final class SettingObserver extends ContentObserver {
    509         public SettingObserver() {
    510             super(new Handler());
    511         }
    512 
    513         @Override
    514         public void onChange(boolean selfChange, Uri uri) {
    515             super.onChange(selfChange, uri);
    516             updateShowPercent();
    517             postInvalidate();
    518         }
    519     }
    520 
    521 }
    522