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