• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BatteryMeterDrawable(Context context, Handler handler, int frameColor)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
getIntrinsicHeight()173     public int getIntrinsicHeight() {
174         return mIntrinsicHeight;
175     }
176 
177     @Override
getIntrinsicWidth()178     public int getIntrinsicWidth() {
179         return mIntrinsicWidth;
180     }
181 
startListening()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 
stopListening()190     public void stopListening() {
191         mListening = false;
192         mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
193         mBatteryController.removeStateChangedCallback(this);
194     }
195 
disableShowPercent()196     public void disableShowPercent() {
197         mShowPercent = false;
198         postInvalidate();
199     }
200 
postInvalidate()201     private void postInvalidate() {
202         mHandler.post(new Runnable() {
203             @Override
204             public void run() {
205                 invalidateSelf();
206             }
207         });
208     }
209 
setBatteryController(BatteryController batteryController)210     public void setBatteryController(BatteryController batteryController) {
211         mBatteryController = batteryController;
212         mPowerSaveEnabled = mBatteryController.isPowerSave();
213     }
214 
215     @Override
onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging)216     public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
217         mLevel = level;
218         mPluggedIn = pluggedIn;
219 
220         postInvalidate();
221     }
222 
223     @Override
onPowerSaveChanged(boolean isPowerSave)224     public void onPowerSaveChanged(boolean isPowerSave) {
225         mPowerSaveEnabled = isPowerSave;
226         invalidateSelf();
227     }
228 
loadBoltPoints(Resources res)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 
loadPlusPoints(Resources res)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
setBounds(int left, int top, int right, int bottom)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 
updateShowPercent()268     private void updateShowPercent() {
269         mShowPercent = 0 != Settings.System.getInt(mContext.getContentResolver(),
270                 SHOW_PERCENT_SETTING, 0);
271     }
272 
getColorForLevel(int percent)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 
setDarkIntensity(float darkIntensity)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 
getBackgroundColor(float darkIntensity)310     private int getBackgroundColor(float darkIntensity) {
311         return getColorForDarkIntensity(
312                 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
313     }
314 
getFillColor(float darkIntensity)315     private int getFillColor(float darkIntensity) {
316         return getColorForDarkIntensity(
317                 darkIntensity, mLightModeFillColor, mDarkModeFillColor);
318     }
319 
getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor)320     private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
321         return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
322     }
323 
324     @Override
draw(Canvas c)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
setAlpha(int alpha)496     public void setAlpha(int alpha) {
497     }
498 
499     @Override
setColorFilter(@ullable ColorFilter colorFilter)500     public void setColorFilter(@Nullable ColorFilter colorFilter) {
501     }
502 
503     @Override
getOpacity()504     public int getOpacity() {
505         return 0;
506     }
507 
508     private final class SettingObserver extends ContentObserver {
SettingObserver()509         public SettingObserver() {
510             super(new Handler());
511         }
512 
513         @Override
onChange(boolean selfChange, Uri uri)514         public void onChange(boolean selfChange, Uri uri) {
515             super.onChange(selfChange, uri);
516             updateShowPercent();
517             postInvalidate();
518         }
519     }
520 
521 }
522