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