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