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
17package com.android.systemui;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.RectF;
29import android.graphics.Typeface;
30import android.os.BatteryManager;
31import android.os.Bundle;
32import android.provider.Settings;
33import android.util.AttributeSet;
34import android.view.View;
35
36import com.android.systemui.statusbar.policy.BatteryController;
37
38public 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