• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BatteryMeterView(Context context)93     public BatteryMeterView(Context context) {
94         this(context, null, 0);
95     }
96 
BatteryMeterView(Context context, AttributeSet attrs)97     public BatteryMeterView(Context context, AttributeSet attrs) {
98         this(context, attrs, 0);
99     }
100 
BatteryMeterView(Context context, AttributeSet attrs, int defStyle)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
onAttachedToWindow()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
onDetachedFromWindow()186     public void onDetachedFromWindow() {
187         super.onDetachedFromWindow();
188 
189         getContext().unregisterReceiver(mTracker);
190         mBatteryController.removeStateChangedCallback(this);
191         getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
192     }
193 
setBatteryController(BatteryController batteryController)194     public void setBatteryController(BatteryController batteryController) {
195         mBatteryController = batteryController;
196         mPowerSaveEnabled = mBatteryController.isPowerSave();
197     }
198 
199     @Override
onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging)200     public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
201         // TODO: Use this callback instead of own broadcast receiver.
202     }
203 
204     @Override
onPowerSaveChanged()205     public void onPowerSaveChanged() {
206         mPowerSaveEnabled = mBatteryController.isPowerSave();
207         invalidate();
208     }
209 
loadBoltPoints(Resources res)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
onSizeChanged(int w, int h, int oldw, int oldh)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 
updateShowPercent()233     private void updateShowPercent() {
234         mShowPercent = 0 != Settings.System.getInt(getContext().getContentResolver(),
235                 SHOW_PERCENT_SETTING, 0);
236     }
237 
getColorForLevel(int percent)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 
setDarkIntensity(float darkIntensity)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 
getBackgroundColor(float darkIntensity)271     private int getBackgroundColor(float darkIntensity) {
272         return getColorForDarkIntensity(
273                 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
274     }
275 
getFillColor(float darkIntensity)276     private int getFillColor(float darkIntensity) {
277         return getColorForDarkIntensity(
278                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
279     }
280 
getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor)281     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
282         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
283     }
284 
285     @Override
draw(Canvas c)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
hasOverlappingRendering()427     public boolean hasOverlappingRendering() {
428         return false;
429     }
430 
431     private boolean mDemoMode;
432     private BatteryTracker mDemoTracker = new BatteryTracker();
433 
434     @Override
dispatchDemoCommand(String command, Bundle args)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
onReceive(Context context, Intent intent)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 {
SettingObserver()531         public SettingObserver() {
532             super(new Handler());
533         }
534 
535         @Override
onChange(boolean selfChange, Uri uri)536         public void onChange(boolean selfChange, Uri uri) {
537             super.onChange(selfChange, uri);
538             updateShowPercent();
539             postInvalidate();
540         }
541     }
542 
543 }
544