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