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