1/*
2 * Copyright (C) 2010 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.settings.fuelgauge;
18
19import android.content.Intent;
20import android.graphics.Bitmap;
21import android.graphics.DashPathEffect;
22import android.os.BatteryManager;
23import android.text.format.DateFormat;
24import android.text.format.Formatter;
25import android.util.Log;
26import android.util.TimeUtils;
27import com.android.settings.R;
28import com.android.settings.Utils;
29
30import android.content.Context;
31import android.content.res.ColorStateList;
32import android.content.res.TypedArray;
33import android.graphics.Canvas;
34import android.graphics.Paint;
35import android.graphics.Path;
36import android.graphics.Typeface;
37import android.os.BatteryStats;
38import android.os.SystemClock;
39import android.os.BatteryStats.HistoryItem;
40import android.telephony.ServiceState;
41import android.text.TextPaint;
42import android.util.AttributeSet;
43import android.util.TypedValue;
44import android.view.View;
45import libcore.icu.LocaleData;
46
47import java.util.ArrayList;
48import java.util.Calendar;
49import java.util.Locale;
50
51public class BatteryHistoryChart extends View {
52    static final boolean DEBUG = false;
53    static final String TAG = "BatteryHistoryChart";
54
55    static final int CHART_DATA_X_MASK = 0x0000ffff;
56    static final int CHART_DATA_BIN_MASK = 0xffff0000;
57    static final int CHART_DATA_BIN_SHIFT = 16;
58
59    static class ChartData {
60        int[] mColors;
61        Paint[] mPaints;
62
63        int mNumTicks;
64        int[] mTicks;
65        int mLastBin;
66
67        void setColors(int[] colors) {
68            mColors = colors;
69            mPaints = new Paint[colors.length];
70            for (int i=0; i<colors.length; i++) {
71                mPaints[i] = new Paint();
72                mPaints[i].setColor(colors[i]);
73                mPaints[i].setStyle(Paint.Style.FILL);
74            }
75        }
76
77        void init(int width) {
78            if (width > 0) {
79                mTicks = new int[width*2];
80            } else {
81                mTicks = null;
82            }
83            mNumTicks = 0;
84            mLastBin = 0;
85        }
86
87        void addTick(int x, int bin) {
88            if (bin != mLastBin && mNumTicks < mTicks.length) {
89                mTicks[mNumTicks] = (x&CHART_DATA_X_MASK) | (bin<<CHART_DATA_BIN_SHIFT);
90                mNumTicks++;
91                mLastBin = bin;
92            }
93        }
94
95        void finish(int width) {
96            if (mLastBin != 0) {
97                addTick(width, 0);
98            }
99        }
100
101        void draw(Canvas canvas, int top, int height) {
102            int lastBin=0, lastX=0;
103            int bottom = top + height;
104            for (int i=0; i<mNumTicks; i++) {
105                int tick = mTicks[i];
106                int x = tick&CHART_DATA_X_MASK;
107                int bin = (tick&CHART_DATA_BIN_MASK) >> CHART_DATA_BIN_SHIFT;
108                if (lastBin != 0) {
109                    canvas.drawRect(lastX, top, x, bottom, mPaints[lastBin]);
110                }
111                lastBin = bin;
112                lastX = x;
113            }
114
115        }
116    }
117
118    static final int SANS = 1;
119    static final int SERIF = 2;
120    static final int MONOSPACE = 3;
121
122    // First value if for phone off; first value is "scanning"; following values
123    // are battery stats signal strength buckets.
124    static final int NUM_PHONE_SIGNALS = 7;
125
126    final Paint mBatteryBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
127    final Paint mBatteryGoodPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
128    final Paint mBatteryWarnPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
129    final Paint mBatteryCriticalPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
130    final Paint mTimeRemainPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
131    final Paint mChargingPaint = new Paint();
132    final Paint mScreenOnPaint = new Paint();
133    final Paint mGpsOnPaint = new Paint();
134    final Paint mFlashlightOnPaint = new Paint();
135    final Paint mCameraOnPaint = new Paint();
136    final Paint mWifiRunningPaint = new Paint();
137    final Paint mCpuRunningPaint = new Paint();
138    final Paint mDateLinePaint = new Paint();
139    final ChartData mPhoneSignalChart = new ChartData();
140    final TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
141    final TextPaint mHeaderTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
142    final Paint mDebugRectPaint = new Paint();
143
144    final Path mBatLevelPath = new Path();
145    final Path mBatGoodPath = new Path();
146    final Path mBatWarnPath = new Path();
147    final Path mBatCriticalPath = new Path();
148    final Path mTimeRemainPath = new Path();
149    final Path mChargingPath = new Path();
150    final Path mScreenOnPath = new Path();
151    final Path mGpsOnPath = new Path();
152    final Path mFlashlightOnPath = new Path();
153    final Path mCameraOnPath = new Path();
154    final Path mWifiRunningPath = new Path();
155    final Path mCpuRunningPath = new Path();
156    final Path mDateLinePath = new Path();
157
158    BatteryStats mStats;
159    Intent mBatteryBroadcast;
160    long mStatsPeriod;
161    int mBatteryLevel;
162    String mMaxPercentLabelString;
163    String mMinPercentLabelString;
164    String mDurationString;
165    String mChargeLabelString;
166    String mChargeDurationString;
167    String mDrainString;
168    String mChargingLabel;
169    String mScreenOnLabel;
170    String mGpsOnLabel;
171    String mCameraOnLabel;
172    String mFlashlightOnLabel;
173    String mWifiRunningLabel;
174    String mCpuRunningLabel;
175    String mPhoneSignalLabel;
176
177    int mChartMinHeight;
178    int mHeaderHeight;
179
180    int mBatteryWarnLevel;
181    int mBatteryCriticalLevel;
182
183    int mTextAscent;
184    int mTextDescent;
185    int mHeaderTextAscent;
186    int mHeaderTextDescent;
187    int mMaxPercentLabelStringWidth;
188    int mMinPercentLabelStringWidth;
189    int mDurationStringWidth;
190    int mChargeLabelStringWidth;
191    int mChargeDurationStringWidth;
192    int mDrainStringWidth;
193
194    boolean mLargeMode;
195
196    int mLastWidth = -1;
197    int mLastHeight = -1;
198
199    int mLineWidth;
200    int mThinLineWidth;
201    int mChargingOffset;
202    int mScreenOnOffset;
203    int mGpsOnOffset;
204    int mFlashlightOnOffset;
205    int mCameraOnOffset;
206    int mWifiRunningOffset;
207    int mCpuRunningOffset;
208    int mPhoneSignalOffset;
209    int mLevelOffset;
210    int mLevelTop;
211    int mLevelBottom;
212    int mLevelLeft;
213    int mLevelRight;
214
215    int mNumHist;
216    long mHistStart;
217    long mHistDataEnd;
218    long mHistEnd;
219    long mStartWallTime;
220    long mEndDataWallTime;
221    long mEndWallTime;
222    boolean mDischarging;
223    int mBatLow;
224    int mBatHigh;
225    boolean mHaveWifi;
226    boolean mHaveGps;
227    boolean mHavePhoneSignal;
228    boolean mHaveCamera;
229    boolean mHaveFlashlight;
230
231    final ArrayList<TimeLabel> mTimeLabels = new ArrayList<TimeLabel>();
232    final ArrayList<DateLabel> mDateLabels = new ArrayList<DateLabel>();
233
234    Bitmap mBitmap;
235    Canvas mCanvas;
236
237    static class TextAttrs {
238        ColorStateList textColor = null;
239        int textSize = 15;
240        int typefaceIndex = -1;
241        int styleIndex = -1;
242
243        void retrieve(Context context, TypedArray from, int index) {
244            TypedArray appearance = null;
245            int ap = from.getResourceId(index, -1);
246            if (ap != -1) {
247                appearance = context.obtainStyledAttributes(ap,
248                                    com.android.internal.R.styleable.TextAppearance);
249            }
250            if (appearance != null) {
251                int n = appearance.getIndexCount();
252                for (int i = 0; i < n; i++) {
253                    int attr = appearance.getIndex(i);
254
255                    switch (attr) {
256                    case com.android.internal.R.styleable.TextAppearance_textColor:
257                        textColor = appearance.getColorStateList(attr);
258                        break;
259
260                    case com.android.internal.R.styleable.TextAppearance_textSize:
261                        textSize = appearance.getDimensionPixelSize(attr, textSize);
262                        break;
263
264                    case com.android.internal.R.styleable.TextAppearance_typeface:
265                        typefaceIndex = appearance.getInt(attr, -1);
266                        break;
267
268                    case com.android.internal.R.styleable.TextAppearance_textStyle:
269                        styleIndex = appearance.getInt(attr, -1);
270                        break;
271                    }
272                }
273
274                appearance.recycle();
275            }
276        }
277
278        void apply(Context context, TextPaint paint) {
279            paint.density = context.getResources().getDisplayMetrics().density;
280            paint.setCompatibilityScaling(
281                    context.getResources().getCompatibilityInfo().applicationScale);
282
283            paint.setColor(textColor.getDefaultColor());
284            paint.setTextSize(textSize);
285
286            Typeface tf = null;
287            switch (typefaceIndex) {
288                case SANS:
289                    tf = Typeface.SANS_SERIF;
290                    break;
291
292                case SERIF:
293                    tf = Typeface.SERIF;
294                    break;
295
296                case MONOSPACE:
297                    tf = Typeface.MONOSPACE;
298                    break;
299            }
300
301            setTypeface(paint, tf, styleIndex);
302        }
303
304        public void setTypeface(TextPaint paint, Typeface tf, int style) {
305            if (style > 0) {
306                if (tf == null) {
307                    tf = Typeface.defaultFromStyle(style);
308                } else {
309                    tf = Typeface.create(tf, style);
310                }
311
312                paint.setTypeface(tf);
313                // now compute what (if any) algorithmic styling is needed
314                int typefaceStyle = tf != null ? tf.getStyle() : 0;
315                int need = style & ~typefaceStyle;
316                paint.setFakeBoldText((need & Typeface.BOLD) != 0);
317                paint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
318            } else {
319                paint.setFakeBoldText(false);
320                paint.setTextSkewX(0);
321                paint.setTypeface(tf);
322            }
323        }
324    }
325
326    static class TimeLabel {
327        final int x;
328        final String label;
329        final int width;
330
331        TimeLabel(TextPaint paint, int x, Calendar cal, boolean use24hr) {
332            this.x = x;
333            final String bestFormat = DateFormat.getBestDateTimePattern(
334                    Locale.getDefault(), use24hr ? "km" : "ha");
335            label = DateFormat.format(bestFormat, cal).toString();
336            width = (int)paint.measureText(label);
337        }
338    }
339
340    static class DateLabel {
341        final int x;
342        final String label;
343        final int width;
344
345        DateLabel(TextPaint paint, int x, Calendar cal, boolean dayFirst) {
346            this.x = x;
347            final String bestFormat = DateFormat.getBestDateTimePattern(
348                    Locale.getDefault(), dayFirst ? "dM" : "Md");
349            label = DateFormat.format(bestFormat, cal).toString();
350            width = (int)paint.measureText(label);
351        }
352    }
353
354    public BatteryHistoryChart(Context context, AttributeSet attrs) {
355        super(context, attrs);
356
357        if (DEBUG) Log.d(TAG, "New BatteryHistoryChart!");
358
359        mBatteryWarnLevel = mContext.getResources().getInteger(
360                com.android.internal.R.integer.config_lowBatteryWarningLevel);
361        mBatteryCriticalLevel = mContext.getResources().getInteger(
362                com.android.internal.R.integer.config_criticalBatteryWarningLevel);
363
364        mThinLineWidth = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
365                2, getResources().getDisplayMetrics());
366
367        mBatteryBackgroundPaint.setColor(0xFF009688);
368        mBatteryBackgroundPaint.setStyle(Paint.Style.FILL);
369        mBatteryGoodPaint.setARGB(128, 0, 128, 0);
370        mBatteryGoodPaint.setStyle(Paint.Style.STROKE);
371        mBatteryWarnPaint.setARGB(128, 128, 128, 0);
372        mBatteryWarnPaint.setStyle(Paint.Style.STROKE);
373        mBatteryCriticalPaint.setARGB(192, 128, 0, 0);
374        mBatteryCriticalPaint.setStyle(Paint.Style.STROKE);
375        mTimeRemainPaint.setColor(0xFFCED7BB);
376        mTimeRemainPaint.setStyle(Paint.Style.FILL);
377        mChargingPaint.setStyle(Paint.Style.STROKE);
378        mScreenOnPaint.setStyle(Paint.Style.STROKE);
379        mGpsOnPaint.setStyle(Paint.Style.STROKE);
380        mCameraOnPaint.setStyle(Paint.Style.STROKE);
381        mFlashlightOnPaint.setStyle(Paint.Style.STROKE);
382        mWifiRunningPaint.setStyle(Paint.Style.STROKE);
383        mCpuRunningPaint.setStyle(Paint.Style.STROKE);
384        mPhoneSignalChart.setColors(com.android.settings.Utils.BADNESS_COLORS);
385        mDebugRectPaint.setARGB(255, 255, 0, 0);
386        mDebugRectPaint.setStyle(Paint.Style.STROKE);
387        mScreenOnPaint.setColor(0xFF009688);
388        mGpsOnPaint.setColor(0xFF009688);
389        mCameraOnPaint.setColor(0xFF009688);
390        mFlashlightOnPaint.setColor(0xFF009688);
391        mWifiRunningPaint.setColor(0xFF009688);
392        mCpuRunningPaint.setColor(0xFF009688);
393        mChargingPaint.setColor(0xFF009688);
394
395        TypedArray a =
396            context.obtainStyledAttributes(
397                attrs, R.styleable.BatteryHistoryChart, 0, 0);
398
399        final TextAttrs mainTextAttrs = new TextAttrs();
400        final TextAttrs headTextAttrs = new TextAttrs();
401        mainTextAttrs.retrieve(context, a, R.styleable.BatteryHistoryChart_android_textAppearance);
402        headTextAttrs.retrieve(context, a, R.styleable.BatteryHistoryChart_headerAppearance);
403
404        int shadowcolor = 0;
405        float dx=0, dy=0, r=0;
406
407        int n = a.getIndexCount();
408        for (int i = 0; i < n; i++) {
409            int attr = a.getIndex(i);
410
411            switch (attr) {
412                case R.styleable.BatteryHistoryChart_android_shadowColor:
413                    shadowcolor = a.getInt(attr, 0);
414                    break;
415
416                case R.styleable.BatteryHistoryChart_android_shadowDx:
417                    dx = a.getFloat(attr, 0);
418                    break;
419
420                case R.styleable.BatteryHistoryChart_android_shadowDy:
421                    dy = a.getFloat(attr, 0);
422                    break;
423
424                case R.styleable.BatteryHistoryChart_android_shadowRadius:
425                    r = a.getFloat(attr, 0);
426                    break;
427
428                case R.styleable.BatteryHistoryChart_android_textColor:
429                    mainTextAttrs.textColor = a.getColorStateList(attr);
430                    headTextAttrs.textColor = a.getColorStateList(attr);
431                    break;
432
433                case R.styleable.BatteryHistoryChart_android_textSize:
434                    mainTextAttrs.textSize = a.getDimensionPixelSize(attr, mainTextAttrs.textSize);
435                    headTextAttrs.textSize = a.getDimensionPixelSize(attr, headTextAttrs.textSize);
436                    break;
437
438                case R.styleable.BatteryHistoryChart_android_typeface:
439                    mainTextAttrs.typefaceIndex = a.getInt(attr, mainTextAttrs.typefaceIndex);
440                    headTextAttrs.typefaceIndex = a.getInt(attr, headTextAttrs.typefaceIndex);
441                    break;
442
443                case R.styleable.BatteryHistoryChart_android_textStyle:
444                    mainTextAttrs.styleIndex = a.getInt(attr, mainTextAttrs.styleIndex);
445                    headTextAttrs.styleIndex = a.getInt(attr, headTextAttrs.styleIndex);
446                    break;
447
448                case R.styleable.BatteryHistoryChart_barPrimaryColor:
449                    mBatteryBackgroundPaint.setColor(a.getInt(attr, 0));
450                    mScreenOnPaint.setColor(a.getInt(attr, 0));
451                    mGpsOnPaint.setColor(a.getInt(attr, 0));
452                    mCameraOnPaint.setColor(a.getInt(attr, 0));
453                    mFlashlightOnPaint.setColor(a.getInt(attr, 0));
454                    mWifiRunningPaint.setColor(a.getInt(attr, 0));
455                    mCpuRunningPaint.setColor(a.getInt(attr, 0));
456                    mChargingPaint.setColor(a.getInt(attr, 0));
457                    break;
458
459                case R.styleable.BatteryHistoryChart_barPredictionColor:
460                    mTimeRemainPaint.setColor(a.getInt(attr, 0));
461                    break;
462
463                case R.styleable.BatteryHistoryChart_chartMinHeight:
464                    mChartMinHeight = a.getDimensionPixelSize(attr, 0);
465                    break;
466            }
467        }
468
469        a.recycle();
470
471        mainTextAttrs.apply(context, mTextPaint);
472        headTextAttrs.apply(context, mHeaderTextPaint);
473
474        mDateLinePaint.set(mTextPaint);
475        mDateLinePaint.setStyle(Paint.Style.STROKE);
476        int hairlineWidth = mThinLineWidth/2;
477        if (hairlineWidth < 1) {
478            hairlineWidth = 1;
479        }
480        mDateLinePaint.setStrokeWidth(hairlineWidth);
481        mDateLinePaint.setPathEffect(new DashPathEffect(new float[] {
482                mThinLineWidth * 2, mThinLineWidth * 2 }, 0));
483
484        if (shadowcolor != 0) {
485            mTextPaint.setShadowLayer(r, dx, dy, shadowcolor);
486            mHeaderTextPaint.setShadowLayer(r, dx, dy, shadowcolor);
487        }
488    }
489
490    void setStats(BatteryStats stats, Intent broadcast) {
491        mStats = stats;
492        mBatteryBroadcast = broadcast;
493
494        if (DEBUG) Log.d(TAG, "Setting stats...");
495
496        final long elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
497
498        long uSecTime = mStats.computeBatteryRealtime(elapsedRealtimeUs,
499                BatteryStats.STATS_SINCE_CHARGED);
500        mStatsPeriod = uSecTime;
501        mChargingLabel = getContext().getString(R.string.battery_stats_charging_label);
502        mScreenOnLabel = getContext().getString(R.string.battery_stats_screen_on_label);
503        mGpsOnLabel = getContext().getString(R.string.battery_stats_gps_on_label);
504        mCameraOnLabel = getContext().getString(R.string.battery_stats_camera_on_label);
505        mFlashlightOnLabel = getContext().getString(R.string.battery_stats_flashlight_on_label);
506        mWifiRunningLabel = getContext().getString(R.string.battery_stats_wifi_running_label);
507        mCpuRunningLabel = getContext().getString(R.string.battery_stats_wake_lock_label);
508        mPhoneSignalLabel = getContext().getString(R.string.battery_stats_phone_signal_label);
509
510        mMaxPercentLabelString = Utils.formatPercentage(100);
511        mMinPercentLabelString = Utils.formatPercentage(0);
512
513        mBatteryLevel = com.android.settings.Utils.getBatteryLevel(mBatteryBroadcast);
514        String batteryPercentString = Utils.formatPercentage(mBatteryLevel);
515        long remainingTimeUs = 0;
516        mDischarging = true;
517        if (mBatteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) == 0) {
518            final long drainTime = mStats.computeBatteryTimeRemaining(elapsedRealtimeUs);
519            if (drainTime > 0) {
520                remainingTimeUs = drainTime;
521                String timeString = Formatter.formatShortElapsedTime(getContext(),
522                        drainTime / 1000);
523                mChargeLabelString = getContext().getResources().getString(
524                        R.string.power_discharging_duration, batteryPercentString, timeString);
525            } else {
526                mChargeLabelString = batteryPercentString;
527            }
528        } else {
529            final long chargeTime = mStats.computeChargeTimeRemaining(elapsedRealtimeUs);
530            final String statusLabel = com.android.settings.Utils.getBatteryStatus(getResources(),
531                    mBatteryBroadcast);
532            final int status = mBatteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS,
533                    BatteryManager.BATTERY_STATUS_UNKNOWN);
534            if (chargeTime > 0 && status != BatteryManager.BATTERY_STATUS_FULL) {
535                mDischarging = false;
536                remainingTimeUs = chargeTime;
537                String timeString = Formatter.formatShortElapsedTime(getContext(),
538                        chargeTime / 1000);
539                int plugType = mBatteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
540                int resId;
541                if (plugType == BatteryManager.BATTERY_PLUGGED_AC) {
542                    resId = R.string.power_charging_duration_ac;
543                } else if (plugType == BatteryManager.BATTERY_PLUGGED_USB) {
544                    resId = R.string.power_charging_duration_usb;
545                } else if (plugType == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
546                    resId = R.string.power_charging_duration_wireless;
547                } else {
548                    resId = R.string.power_charging_duration;
549                }
550                mChargeLabelString = getContext().getResources().getString(
551                        resId, batteryPercentString, timeString);
552            } else {
553                mChargeLabelString = getContext().getResources().getString(
554                        R.string.power_charging, batteryPercentString, statusLabel);
555            }
556        }
557        mDrainString = "";
558        mChargeDurationString = "";
559        setContentDescription(mChargeLabelString);
560
561        int pos = 0;
562        int lastInteresting = 0;
563        byte lastLevel = -1;
564        mBatLow = 0;
565        mBatHigh = 100;
566        mStartWallTime = 0;
567        mEndDataWallTime = 0;
568        mEndWallTime = 0;
569        mHistStart = 0;
570        mHistEnd = 0;
571        long lastWallTime = 0;
572        long lastRealtime = 0;
573        int aggrStates = 0;
574        int aggrStates2 = 0;
575        boolean first = true;
576        if (stats.startIteratingHistoryLocked()) {
577            final HistoryItem rec = new HistoryItem();
578            while (stats.getNextHistoryLocked(rec)) {
579                pos++;
580                if (first) {
581                    first = false;
582                    mHistStart = rec.time;
583                }
584                if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
585                        || rec.cmd == HistoryItem.CMD_RESET) {
586                    // If there is a ridiculously large jump in time, then we won't be
587                    // able to create a good chart with that data, so just ignore the
588                    // times we got before and pretend like our data extends back from
589                    // the time we have now.
590                    // Also, if we are getting a time change and we are less than 5 minutes
591                    // since the start of the history real time, then also use this new
592                    // time to compute the base time, since whatever time we had before is
593                    // pretty much just noise.
594                    if (rec.currentTime > (lastWallTime+(180*24*60*60*1000L))
595                            || rec.time < (mHistStart+(5*60*1000L))) {
596                        mStartWallTime = 0;
597                    }
598                    lastWallTime = rec.currentTime;
599                    lastRealtime = rec.time;
600                    if (mStartWallTime == 0) {
601                        mStartWallTime = lastWallTime - (lastRealtime-mHistStart);
602                    }
603                }
604                if (rec.isDeltaData()) {
605                    if (rec.batteryLevel != lastLevel || pos == 1) {
606                        lastLevel = rec.batteryLevel;
607                    }
608                    lastInteresting = pos;
609                    mHistDataEnd = rec.time;
610                    aggrStates |= rec.states;
611                    aggrStates2 |= rec.states2;
612                }
613            }
614        }
615        mHistEnd = mHistDataEnd + (remainingTimeUs/1000);
616        mEndDataWallTime = lastWallTime + mHistDataEnd - lastRealtime;
617        mEndWallTime = mEndDataWallTime + (remainingTimeUs/1000);
618        mNumHist = lastInteresting;
619        mHaveGps = (aggrStates&HistoryItem.STATE_GPS_ON_FLAG) != 0;
620        mHaveFlashlight = (aggrStates2&HistoryItem.STATE2_FLASHLIGHT_FLAG) != 0;
621        mHaveCamera = (aggrStates2&HistoryItem.STATE2_CAMERA_FLAG) != 0;
622        mHaveWifi = (aggrStates2&HistoryItem.STATE2_WIFI_RUNNING_FLAG) != 0
623                || (aggrStates&(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG
624                        |HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG
625                        |HistoryItem.STATE_WIFI_SCAN_FLAG)) != 0;
626        if (!com.android.settings.Utils.isWifiOnly(getContext())) {
627            mHavePhoneSignal = true;
628        }
629        if (mHistEnd <= mHistStart) mHistEnd = mHistStart+1;
630    }
631
632    @Override
633    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
634        mMaxPercentLabelStringWidth = (int)mTextPaint.measureText(mMaxPercentLabelString);
635        mMinPercentLabelStringWidth = (int)mTextPaint.measureText(mMinPercentLabelString);
636        mDrainStringWidth = (int)mHeaderTextPaint.measureText(mDrainString);
637        mChargeLabelStringWidth = (int)mHeaderTextPaint.measureText(mChargeLabelString);
638        mChargeDurationStringWidth = (int)mHeaderTextPaint.measureText(mChargeDurationString);
639        mTextAscent = (int)mTextPaint.ascent();
640        mTextDescent = (int)mTextPaint.descent();
641        mHeaderTextAscent = (int)mHeaderTextPaint.ascent();
642        mHeaderTextDescent = (int)mHeaderTextPaint.descent();
643        int headerTextHeight = mHeaderTextDescent - mHeaderTextAscent;
644        mHeaderHeight = headerTextHeight*2 - mTextAscent;
645        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
646                getDefaultSize(mChartMinHeight+mHeaderHeight, heightMeasureSpec));
647    }
648
649    void finishPaths(int w, int h, int levelh, int startX, int y, Path curLevelPath,
650            int lastX, boolean lastCharging, boolean lastScreenOn, boolean lastGpsOn,
651            boolean lastFlashlightOn, boolean lastCameraOn, boolean lastWifiRunning,
652            boolean lastCpuRunning, Path lastPath) {
653        if (curLevelPath != null) {
654            if (lastX >= 0 && lastX < w) {
655                if (lastPath != null) {
656                    lastPath.lineTo(w, y);
657                }
658                curLevelPath.lineTo(w, y);
659            }
660            curLevelPath.lineTo(w, mLevelTop+levelh);
661            curLevelPath.lineTo(startX, mLevelTop+levelh);
662            curLevelPath.close();
663        }
664
665        if (lastCharging) {
666            mChargingPath.lineTo(w, h-mChargingOffset);
667        }
668        if (lastScreenOn) {
669            mScreenOnPath.lineTo(w, h-mScreenOnOffset);
670        }
671        if (lastGpsOn) {
672            mGpsOnPath.lineTo(w, h-mGpsOnOffset);
673        }
674        if (lastFlashlightOn) {
675            mFlashlightOnPath.lineTo(w, h-mFlashlightOnOffset);
676        }
677        if (lastCameraOn) {
678            mCameraOnPath.lineTo(w, h-mCameraOnOffset);
679        }
680        if (lastWifiRunning) {
681            mWifiRunningPath.lineTo(w, h-mWifiRunningOffset);
682        }
683        if (lastCpuRunning) {
684            mCpuRunningPath.lineTo(w, h - mCpuRunningOffset);
685        }
686        if (mHavePhoneSignal) {
687            mPhoneSignalChart.finish(w);
688        }
689    }
690
691    private boolean is24Hour() {
692        return DateFormat.is24HourFormat(getContext());
693    }
694
695    private boolean isDayFirst() {
696        final String value = LocaleData.get(getResources().getConfiguration().locale)
697                .getDateFormat(java.text.DateFormat.SHORT);
698        return value.indexOf('M') > value.indexOf('d');
699    }
700
701    @Override
702    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
703        super.onSizeChanged(w, h, oldw, oldh);
704
705        if (DEBUG) Log.d(TAG, "onSizeChanged: " + oldw + "x" + oldh + " to " + w + "x" + h);
706
707        if (mLastWidth == w && mLastHeight == h) {
708            return;
709        }
710
711        if (mLastWidth == 0 || mLastHeight == 0) {
712            return;
713        }
714
715        if (DEBUG) Log.d(TAG, "Rebuilding chart for: " + w + "x" + h);
716
717        mLastWidth = w;
718        mLastHeight = h;
719        mBitmap = null;
720        mCanvas = null;
721
722        int textHeight = mTextDescent - mTextAscent;
723        if (h > ((textHeight*10)+mChartMinHeight)) {
724            mLargeMode = true;
725            if (h > (textHeight*15)) {
726                // Plenty of room for the chart.
727                mLineWidth = textHeight/2;
728            } else {
729                // Compress lines to make more room for chart.
730                mLineWidth = textHeight/3;
731            }
732        } else {
733            mLargeMode = false;
734            mLineWidth = mThinLineWidth;
735        }
736        if (mLineWidth <= 0) mLineWidth = 1;
737
738        mLevelTop = mHeaderHeight;
739        mLevelLeft = mMaxPercentLabelStringWidth + mThinLineWidth*3;
740        mLevelRight = w;
741        int levelWidth = mLevelRight-mLevelLeft;
742
743        mTextPaint.setStrokeWidth(mThinLineWidth);
744        mBatteryGoodPaint.setStrokeWidth(mThinLineWidth);
745        mBatteryWarnPaint.setStrokeWidth(mThinLineWidth);
746        mBatteryCriticalPaint.setStrokeWidth(mThinLineWidth);
747        mChargingPaint.setStrokeWidth(mLineWidth);
748        mScreenOnPaint.setStrokeWidth(mLineWidth);
749        mGpsOnPaint.setStrokeWidth(mLineWidth);
750        mCameraOnPaint.setStrokeWidth(mLineWidth);
751        mFlashlightOnPaint.setStrokeWidth(mLineWidth);
752        mWifiRunningPaint.setStrokeWidth(mLineWidth);
753        mCpuRunningPaint.setStrokeWidth(mLineWidth);
754        mDebugRectPaint.setStrokeWidth(1);
755
756        int fullBarOffset = textHeight + mLineWidth;
757
758        if (mLargeMode) {
759            mChargingOffset = mLineWidth;
760            mScreenOnOffset = mChargingOffset + fullBarOffset;
761            mCpuRunningOffset = mScreenOnOffset + fullBarOffset;
762            mWifiRunningOffset = mCpuRunningOffset + fullBarOffset;
763            mGpsOnOffset = mWifiRunningOffset + (mHaveWifi ? fullBarOffset : 0);
764            mFlashlightOnOffset = mGpsOnOffset + (mHaveGps ? fullBarOffset : 0);
765            mCameraOnOffset = mFlashlightOnOffset + (mHaveFlashlight ? fullBarOffset : 0);
766            mPhoneSignalOffset = mCameraOnOffset + (mHaveCamera ? fullBarOffset : 0);
767            mLevelOffset = mPhoneSignalOffset + (mHavePhoneSignal ? fullBarOffset : 0)
768                    + mLineWidth*2 + mLineWidth/2;
769            if (mHavePhoneSignal) {
770                mPhoneSignalChart.init(w);
771            }
772        } else {
773            mScreenOnOffset = mGpsOnOffset = mCameraOnOffset = mFlashlightOnOffset =
774                    mWifiRunningOffset = mCpuRunningOffset = mChargingOffset =
775                    mPhoneSignalOffset = 0;
776            mLevelOffset = fullBarOffset + mThinLineWidth*4;
777            if (mHavePhoneSignal) {
778                mPhoneSignalChart.init(0);
779            }
780        }
781
782        mBatLevelPath.reset();
783        mBatGoodPath.reset();
784        mBatWarnPath.reset();
785        mTimeRemainPath.reset();
786        mBatCriticalPath.reset();
787        mScreenOnPath.reset();
788        mGpsOnPath.reset();
789        mFlashlightOnPath.reset();
790        mCameraOnPath.reset();
791        mWifiRunningPath.reset();
792        mCpuRunningPath.reset();
793        mChargingPath.reset();
794
795        mTimeLabels.clear();
796        mDateLabels.clear();
797
798        final long walltimeStart = mStartWallTime;
799        final long walltimeChange = mEndWallTime > walltimeStart
800                ? (mEndWallTime-walltimeStart) : 1;
801        long curWalltime = mStartWallTime;
802        long lastRealtime = 0;
803
804        final int batLow = mBatLow;
805        final int batChange = mBatHigh-mBatLow;
806
807        final int levelh = h - mLevelOffset - mLevelTop;
808        mLevelBottom = mLevelTop + levelh;
809
810        int x = mLevelLeft, y = 0, startX = mLevelLeft, lastX = -1, lastY = -1;
811        int i = 0;
812        Path curLevelPath = null;
813        Path lastLinePath = null;
814        boolean lastCharging = false, lastScreenOn = false, lastGpsOn = false;
815        boolean lastFlashlightOn = false, lastCameraOn = false;
816        boolean lastWifiRunning = false, lastWifiSupplRunning = false, lastCpuRunning = false;
817        int lastWifiSupplState = BatteryStats.WIFI_SUPPL_STATE_INVALID;
818        final int N = mNumHist;
819        if (mEndDataWallTime > mStartWallTime && mStats.startIteratingHistoryLocked()) {
820            final HistoryItem rec = new HistoryItem();
821            while (mStats.getNextHistoryLocked(rec) && i < N) {
822                if (rec.isDeltaData()) {
823                    curWalltime += rec.time-lastRealtime;
824                    lastRealtime = rec.time;
825                    x = mLevelLeft + (int)(((curWalltime-walltimeStart)*levelWidth)/walltimeChange);
826                    if (x < 0) {
827                        x = 0;
828                    }
829                    if (false) {
830                        StringBuilder sb = new StringBuilder(128);
831                        sb.append("walloff=");
832                        TimeUtils.formatDuration(curWalltime - walltimeStart, sb);
833                        sb.append(" wallchange=");
834                        TimeUtils.formatDuration(walltimeChange, sb);
835                        sb.append(" x=");
836                        sb.append(x);
837                        Log.d("foo", sb.toString());
838                    }
839                    y = mLevelTop + levelh - ((rec.batteryLevel-batLow)*(levelh-1))/batChange;
840
841                    if (lastX != x) {
842                        // We have moved by at least a pixel.
843                        if (lastY != y) {
844                            // Don't plot changes within a pixel.
845                            Path path;
846                            byte value = rec.batteryLevel;
847                            if (value <= mBatteryCriticalLevel) path = mBatCriticalPath;
848                            else if (value <= mBatteryWarnLevel) path = mBatWarnPath;
849                            else path = null; //mBatGoodPath;
850
851                            if (path != lastLinePath) {
852                                if (lastLinePath != null) {
853                                    lastLinePath.lineTo(x, y);
854                                }
855                                if (path != null) {
856                                    path.moveTo(x, y);
857                                }
858                                lastLinePath = path;
859                            } else if (path != null) {
860                                path.lineTo(x, y);
861                            }
862
863                            if (curLevelPath == null) {
864                                curLevelPath = mBatLevelPath;
865                                curLevelPath.moveTo(x, y);
866                                startX = x;
867                            } else {
868                                curLevelPath.lineTo(x, y);
869                            }
870                            lastX = x;
871                            lastY = y;
872                        }
873                    }
874
875                    if (mLargeMode) {
876                        final boolean charging =
877                            (rec.states&HistoryItem.STATE_BATTERY_PLUGGED_FLAG) != 0;
878                        if (charging != lastCharging) {
879                            if (charging) {
880                                mChargingPath.moveTo(x, h-mChargingOffset);
881                            } else {
882                                mChargingPath.lineTo(x, h-mChargingOffset);
883                            }
884                            lastCharging = charging;
885                        }
886
887                        final boolean screenOn =
888                            (rec.states&HistoryItem.STATE_SCREEN_ON_FLAG) != 0;
889                        if (screenOn != lastScreenOn) {
890                            if (screenOn) {
891                                mScreenOnPath.moveTo(x, h-mScreenOnOffset);
892                            } else {
893                                mScreenOnPath.lineTo(x, h-mScreenOnOffset);
894                            }
895                            lastScreenOn = screenOn;
896                        }
897
898                        final boolean gpsOn =
899                            (rec.states&HistoryItem.STATE_GPS_ON_FLAG) != 0;
900                        if (gpsOn != lastGpsOn) {
901                            if (gpsOn) {
902                                mGpsOnPath.moveTo(x, h-mGpsOnOffset);
903                            } else {
904                                mGpsOnPath.lineTo(x, h-mGpsOnOffset);
905                            }
906                            lastGpsOn = gpsOn;
907                        }
908
909                        final boolean flashlightOn =
910                            (rec.states2&HistoryItem.STATE2_FLASHLIGHT_FLAG) != 0;
911                        if (flashlightOn != lastFlashlightOn) {
912                            if (flashlightOn) {
913                                mFlashlightOnPath.moveTo(x, h-mFlashlightOnOffset);
914                            } else {
915                                mFlashlightOnPath.lineTo(x, h-mFlashlightOnOffset);
916                            }
917                            lastFlashlightOn = flashlightOn;
918                        }
919
920                        final boolean cameraOn =
921                            (rec.states2&HistoryItem.STATE2_CAMERA_FLAG) != 0;
922                        if (cameraOn != lastCameraOn) {
923                            if (cameraOn) {
924                                mCameraOnPath.moveTo(x, h-mCameraOnOffset);
925                            } else {
926                                mCameraOnPath.lineTo(x, h-mCameraOnOffset);
927                            }
928                            lastCameraOn = cameraOn;
929                        }
930
931                        final int wifiSupplState =
932                            ((rec.states2&HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK)
933                                    >> HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT);
934                        boolean wifiRunning;
935                        if (lastWifiSupplState != wifiSupplState) {
936                            lastWifiSupplState = wifiSupplState;
937                            switch (wifiSupplState) {
938                                case BatteryStats.WIFI_SUPPL_STATE_DISCONNECTED:
939                                case BatteryStats.WIFI_SUPPL_STATE_DORMANT:
940                                case BatteryStats.WIFI_SUPPL_STATE_INACTIVE:
941                                case BatteryStats.WIFI_SUPPL_STATE_INTERFACE_DISABLED:
942                                case BatteryStats.WIFI_SUPPL_STATE_INVALID:
943                                case BatteryStats.WIFI_SUPPL_STATE_UNINITIALIZED:
944                                    wifiRunning = lastWifiSupplRunning = false;
945                                    break;
946                                default:
947                                    wifiRunning = lastWifiSupplRunning = true;
948                                    break;
949                            }
950                        } else {
951                            wifiRunning = lastWifiSupplRunning;
952                        }
953                        if ((rec.states&(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG
954                                |HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG
955                                |HistoryItem.STATE_WIFI_SCAN_FLAG)) != 0) {
956                            wifiRunning = true;
957                        }
958                        if (wifiRunning != lastWifiRunning) {
959                            if (wifiRunning) {
960                                mWifiRunningPath.moveTo(x, h-mWifiRunningOffset);
961                            } else {
962                                mWifiRunningPath.lineTo(x, h-mWifiRunningOffset);
963                            }
964                            lastWifiRunning = wifiRunning;
965                        }
966
967                        final boolean cpuRunning =
968                            (rec.states&HistoryItem.STATE_CPU_RUNNING_FLAG) != 0;
969                        if (cpuRunning != lastCpuRunning) {
970                            if (cpuRunning) {
971                                mCpuRunningPath.moveTo(x, h - mCpuRunningOffset);
972                            } else {
973                                mCpuRunningPath.lineTo(x, h - mCpuRunningOffset);
974                            }
975                            lastCpuRunning = cpuRunning;
976                        }
977
978                        if (mLargeMode && mHavePhoneSignal) {
979                            int bin;
980                            if (((rec.states&HistoryItem.STATE_PHONE_STATE_MASK)
981                                    >> HistoryItem.STATE_PHONE_STATE_SHIFT)
982                                    == ServiceState.STATE_POWER_OFF) {
983                                bin = 0;
984                            } else if ((rec.states&HistoryItem.STATE_PHONE_SCANNING_FLAG) != 0) {
985                                bin = 1;
986                            } else {
987                                bin = (rec.states&HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK)
988                                        >> HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT;
989                                bin += 2;
990                            }
991                            mPhoneSignalChart.addTick(x, bin);
992                        }
993                    }
994
995                } else {
996                    long lastWalltime = curWalltime;
997                    if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
998                            || rec.cmd == HistoryItem.CMD_RESET) {
999                        if (rec.currentTime >= mStartWallTime) {
1000                            curWalltime = rec.currentTime;
1001                        } else {
1002                            curWalltime = mStartWallTime + (rec.time-mHistStart);
1003                        }
1004                        lastRealtime = rec.time;
1005                    }
1006
1007                    if (rec.cmd != HistoryItem.CMD_OVERFLOW
1008                            && (rec.cmd != HistoryItem.CMD_CURRENT_TIME
1009                                    || Math.abs(lastWalltime-curWalltime) > (60*60*1000))) {
1010                        if (curLevelPath != null) {
1011                            finishPaths(x+1, h, levelh, startX, lastY, curLevelPath, lastX,
1012                                    lastCharging, lastScreenOn, lastGpsOn, lastFlashlightOn,
1013                                    lastCameraOn, lastWifiRunning, lastCpuRunning, lastLinePath);
1014                            lastX = lastY = -1;
1015                            curLevelPath = null;
1016                            lastLinePath = null;
1017                            lastCharging = lastScreenOn = lastGpsOn = lastFlashlightOn =
1018                                    lastCameraOn = lastCpuRunning = false;
1019                        }
1020                    }
1021                }
1022
1023                i++;
1024            }
1025            mStats.finishIteratingHistoryLocked();
1026        }
1027
1028        if (lastY < 0 || lastX < 0) {
1029            // Didn't get any data...
1030            x = lastX = mLevelLeft;
1031            y = lastY = mLevelTop + levelh - ((mBatteryLevel-batLow)*(levelh-1))/batChange;
1032            Path path;
1033            byte value = (byte)mBatteryLevel;
1034            if (value <= mBatteryCriticalLevel) path = mBatCriticalPath;
1035            else if (value <= mBatteryWarnLevel) path = mBatWarnPath;
1036            else path = null; //mBatGoodPath;
1037            if (path != null) {
1038                path.moveTo(x, y);
1039                lastLinePath = path;
1040            }
1041            mBatLevelPath.moveTo(x, y);
1042            curLevelPath = mBatLevelPath;
1043            x = w;
1044        } else {
1045            // Figure out where the actual data ends on the screen.
1046            x = mLevelLeft + (int)(((mEndDataWallTime-walltimeStart)*levelWidth)/walltimeChange);
1047            if (x < 0) {
1048                x = 0;
1049            }
1050        }
1051
1052        finishPaths(x, h, levelh, startX, lastY, curLevelPath, lastX,
1053                lastCharging, lastScreenOn, lastGpsOn, lastFlashlightOn, lastCameraOn,
1054                lastWifiRunning, lastCpuRunning, lastLinePath);
1055
1056        if (x < w) {
1057            // If we reserved room for the remaining time, create a final path to draw
1058            // that part of the UI.
1059            mTimeRemainPath.moveTo(x, lastY);
1060            int fullY = mLevelTop + levelh - ((100-batLow)*(levelh-1))/batChange;
1061            int emptyY = mLevelTop + levelh - ((0-batLow)*(levelh-1))/batChange;
1062            if (mDischarging) {
1063                mTimeRemainPath.lineTo(mLevelRight, emptyY);
1064            } else {
1065                mTimeRemainPath.lineTo(mLevelRight, fullY);
1066                mTimeRemainPath.lineTo(mLevelRight, emptyY);
1067            }
1068            mTimeRemainPath.lineTo(x, emptyY);
1069            mTimeRemainPath.close();
1070        }
1071
1072        if (mStartWallTime > 0 && mEndWallTime > mStartWallTime) {
1073            // Create the time labels at the bottom.
1074            boolean is24hr = is24Hour();
1075            Calendar calStart = Calendar.getInstance();
1076            calStart.setTimeInMillis(mStartWallTime);
1077            calStart.set(Calendar.MILLISECOND, 0);
1078            calStart.set(Calendar.SECOND, 0);
1079            calStart.set(Calendar.MINUTE, 0);
1080            long startRoundTime = calStart.getTimeInMillis();
1081            if (startRoundTime < mStartWallTime) {
1082                calStart.set(Calendar.HOUR_OF_DAY, calStart.get(Calendar.HOUR_OF_DAY)+1);
1083                startRoundTime = calStart.getTimeInMillis();
1084            }
1085            Calendar calEnd = Calendar.getInstance();
1086            calEnd.setTimeInMillis(mEndWallTime);
1087            calEnd.set(Calendar.MILLISECOND, 0);
1088            calEnd.set(Calendar.SECOND, 0);
1089            calEnd.set(Calendar.MINUTE, 0);
1090            long endRoundTime = calEnd.getTimeInMillis();
1091            if (startRoundTime < endRoundTime) {
1092                addTimeLabel(calStart, mLevelLeft, mLevelRight, is24hr);
1093                Calendar calMid = Calendar.getInstance();
1094                calMid.setTimeInMillis(mStartWallTime+((mEndWallTime-mStartWallTime)/2));
1095                calMid.set(Calendar.MILLISECOND, 0);
1096                calMid.set(Calendar.SECOND, 0);
1097                calMid.set(Calendar.MINUTE, 0);
1098                long calMidMillis = calMid.getTimeInMillis();
1099                if (calMidMillis > startRoundTime && calMidMillis < endRoundTime) {
1100                    addTimeLabel(calMid, mLevelLeft, mLevelRight, is24hr);
1101                }
1102                addTimeLabel(calEnd, mLevelLeft, mLevelRight, is24hr);
1103            }
1104
1105            // Create the date labels if the chart includes multiple days
1106            if (calStart.get(Calendar.DAY_OF_YEAR) != calEnd.get(Calendar.DAY_OF_YEAR) ||
1107                    calStart.get(Calendar.YEAR) != calEnd.get(Calendar.YEAR)) {
1108                boolean isDayFirst = isDayFirst();
1109                calStart.set(Calendar.HOUR_OF_DAY, 0);
1110                startRoundTime = calStart.getTimeInMillis();
1111                if (startRoundTime < mStartWallTime) {
1112                    calStart.set(Calendar.DAY_OF_YEAR, calStart.get(Calendar.DAY_OF_YEAR) + 1);
1113                    startRoundTime = calStart.getTimeInMillis();
1114                }
1115                calEnd.set(Calendar.HOUR_OF_DAY, 0);
1116                endRoundTime = calEnd.getTimeInMillis();
1117                if (startRoundTime < endRoundTime) {
1118                    addDateLabel(calStart, mLevelLeft, mLevelRight, isDayFirst);
1119                    Calendar calMid = Calendar.getInstance();
1120                    calMid.setTimeInMillis(startRoundTime + ((endRoundTime - startRoundTime) / 2));
1121                    calMid.set(Calendar.HOUR_OF_DAY, 0);
1122                    long calMidMillis = calMid.getTimeInMillis();
1123                    if (calMidMillis > startRoundTime && calMidMillis < endRoundTime) {
1124                        addDateLabel(calMid, mLevelLeft, mLevelRight, isDayFirst);
1125                    }
1126                }
1127                addDateLabel(calEnd, mLevelLeft, mLevelRight, isDayFirst);
1128            }
1129        }
1130
1131        if (mTimeLabels.size() < 2) {
1132            // If there are fewer than 2 time labels, then they are useless.  Just
1133            // show an axis label giving the entire duration.
1134            mDurationString = Formatter.formatShortElapsedTime(getContext(),
1135                    mEndWallTime - mStartWallTime);
1136            mDurationStringWidth = (int)mTextPaint.measureText(mDurationString);
1137        } else {
1138            mDurationString = null;
1139            mDurationStringWidth = 0;
1140        }
1141    }
1142
1143    void addTimeLabel(Calendar cal, int levelLeft, int levelRight, boolean is24hr) {
1144        final long walltimeStart = mStartWallTime;
1145        final long walltimeChange = mEndWallTime-walltimeStart;
1146        mTimeLabels.add(new TimeLabel(mTextPaint,
1147                levelLeft + (int)(((cal.getTimeInMillis()-walltimeStart)*(levelRight-levelLeft))
1148                        / walltimeChange),
1149                cal, is24hr));
1150    }
1151
1152    void addDateLabel(Calendar cal, int levelLeft, int levelRight, boolean isDayFirst) {
1153        final long walltimeStart = mStartWallTime;
1154        final long walltimeChange = mEndWallTime-walltimeStart;
1155        mDateLabels.add(new DateLabel(mTextPaint,
1156                levelLeft + (int)(((cal.getTimeInMillis()-walltimeStart)*(levelRight-levelLeft))
1157                        / walltimeChange),
1158                cal, isDayFirst));
1159    }
1160
1161    @Override
1162    protected void onDraw(Canvas canvas) {
1163        super.onDraw(canvas);
1164
1165        final int width = getWidth();
1166        final int height = getHeight();
1167
1168        //buildBitmap(width, height);
1169
1170        if (DEBUG) Log.d(TAG, "onDraw: " + width + "x" + height);
1171        //canvas.drawBitmap(mBitmap, 0, 0, null);
1172        drawChart(canvas, width, height);
1173    }
1174
1175    void buildBitmap(int width, int height) {
1176        if (mBitmap != null && width == mBitmap.getWidth() && height == mBitmap.getHeight()) {
1177            return;
1178        }
1179
1180        if (DEBUG) Log.d(TAG, "buildBitmap: " + width + "x" + height);
1181
1182        mBitmap = Bitmap.createBitmap(getResources().getDisplayMetrics(), width, height,
1183                Bitmap.Config.ARGB_8888);
1184        mCanvas = new Canvas(mBitmap);
1185        drawChart(mCanvas, width, height);
1186    }
1187
1188    void drawChart(Canvas canvas, int width, int height) {
1189        final boolean layoutRtl = isLayoutRtl();
1190        final int textStartX = layoutRtl ? width : 0;
1191        final int textEndX = layoutRtl ? 0 : width;
1192        final Paint.Align textAlignLeft = layoutRtl ? Paint.Align.RIGHT : Paint.Align.LEFT;
1193        final Paint.Align textAlignRight = layoutRtl ? Paint.Align.LEFT : Paint.Align.RIGHT;
1194
1195        if (DEBUG) {
1196            canvas.drawRect(1, 1, width, height, mDebugRectPaint);
1197        }
1198
1199        if (DEBUG) Log.d(TAG, "Drawing level path.");
1200        canvas.drawPath(mBatLevelPath, mBatteryBackgroundPaint);
1201        if (!mTimeRemainPath.isEmpty()) {
1202            if (DEBUG) Log.d(TAG, "Drawing time remain path.");
1203            canvas.drawPath(mTimeRemainPath, mTimeRemainPaint);
1204        }
1205        if (mTimeLabels.size() > 1) {
1206            int y = mLevelBottom - mTextAscent + (mThinLineWidth*4);
1207            int ytick = mLevelBottom+mThinLineWidth+(mThinLineWidth/2);
1208            mTextPaint.setTextAlign(Paint.Align.LEFT);
1209            int lastX = 0;
1210            for (int i=0; i<mTimeLabels.size(); i++) {
1211                TimeLabel label = mTimeLabels.get(i);
1212                if (i == 0) {
1213                    int x = label.x - label.width/2;
1214                    if (x < 0) {
1215                        x = 0;
1216                    }
1217                    if (DEBUG) Log.d(TAG, "Drawing left label: " + label.label + " @ " + x);
1218                    canvas.drawText(label.label, x, y, mTextPaint);
1219                    canvas.drawLine(label.x, ytick, label.x, ytick+mThinLineWidth, mTextPaint);
1220                    lastX = x + label.width;
1221                } else if (i < (mTimeLabels.size()-1)) {
1222                    int x = label.x - label.width/2;
1223                    if (x < (lastX+mTextAscent)) {
1224                        continue;
1225                    }
1226                    TimeLabel nextLabel = mTimeLabels.get(i+1);
1227                    if (x > (width-nextLabel.width-mTextAscent)) {
1228                        continue;
1229                    }
1230                    if (DEBUG) Log.d(TAG, "Drawing middle label: " + label.label + " @ " + x);
1231                    canvas.drawText(label.label, x, y, mTextPaint);
1232                    canvas.drawLine(label.x, ytick, label.x, ytick + mThinLineWidth, mTextPaint);
1233                    lastX = x + label.width;
1234                } else {
1235                    int x = label.x - label.width/2;
1236                    if ((x+label.width) >= width) {
1237                        x = width-1-label.width;
1238                    }
1239                    if (DEBUG) Log.d(TAG, "Drawing right label: " + label.label + " @ " + x);
1240                    canvas.drawText(label.label, x, y, mTextPaint);
1241                    canvas.drawLine(label.x, ytick, label.x, ytick+mThinLineWidth, mTextPaint);
1242                }
1243            }
1244        } else if (mDurationString != null) {
1245            int y = mLevelBottom - mTextAscent + (mThinLineWidth*4);
1246            mTextPaint.setTextAlign(Paint.Align.LEFT);
1247            canvas.drawText(mDurationString,
1248                    mLevelLeft + (mLevelRight-mLevelLeft)/2 - mDurationStringWidth/2,
1249                    y, mTextPaint);
1250        }
1251
1252        int headerTop = -mHeaderTextAscent + (mHeaderTextDescent-mHeaderTextAscent)/3;
1253        mHeaderTextPaint.setTextAlign(textAlignLeft);
1254        if (DEBUG) Log.d(TAG, "Drawing charge label string: " + mChargeLabelString);
1255        canvas.drawText(mChargeLabelString, textStartX, headerTop, mHeaderTextPaint);
1256        int stringHalfWidth = mChargeDurationStringWidth / 2;
1257        if (layoutRtl) stringHalfWidth = -stringHalfWidth;
1258        int headerCenter = ((width-mChargeDurationStringWidth-mDrainStringWidth)/2)
1259                + (layoutRtl ? mDrainStringWidth : mChargeLabelStringWidth);
1260        if (DEBUG) Log.d(TAG, "Drawing charge duration string: " + mChargeDurationString);
1261        canvas.drawText(mChargeDurationString, headerCenter - stringHalfWidth, headerTop,
1262                mHeaderTextPaint);
1263        mHeaderTextPaint.setTextAlign(textAlignRight);
1264        if (DEBUG) Log.d(TAG, "Drawing drain string: " + mDrainString);
1265        canvas.drawText(mDrainString, textEndX, headerTop, mHeaderTextPaint);
1266
1267        if (!mBatGoodPath.isEmpty()) {
1268            if (DEBUG) Log.d(TAG, "Drawing good battery path");
1269            canvas.drawPath(mBatGoodPath, mBatteryGoodPaint);
1270        }
1271        if (!mBatWarnPath.isEmpty()) {
1272            if (DEBUG) Log.d(TAG, "Drawing warn battery path");
1273            canvas.drawPath(mBatWarnPath, mBatteryWarnPaint);
1274        }
1275        if (!mBatCriticalPath.isEmpty()) {
1276            if (DEBUG) Log.d(TAG, "Drawing critical battery path");
1277            canvas.drawPath(mBatCriticalPath, mBatteryCriticalPaint);
1278        }
1279        if (mHavePhoneSignal) {
1280            if (DEBUG) Log.d(TAG, "Drawing phone signal path");
1281            int top = height-mPhoneSignalOffset - (mLineWidth/2);
1282            mPhoneSignalChart.draw(canvas, top, mLineWidth);
1283        }
1284        if (!mScreenOnPath.isEmpty()) {
1285            if (DEBUG) Log.d(TAG, "Drawing screen on path");
1286            canvas.drawPath(mScreenOnPath, mScreenOnPaint);
1287        }
1288        if (!mChargingPath.isEmpty()) {
1289            if (DEBUG) Log.d(TAG, "Drawing charging path");
1290            canvas.drawPath(mChargingPath, mChargingPaint);
1291        }
1292        if (mHaveGps) {
1293            if (!mGpsOnPath.isEmpty()) {
1294                if (DEBUG) Log.d(TAG, "Drawing gps path");
1295                canvas.drawPath(mGpsOnPath, mGpsOnPaint);
1296            }
1297        }
1298        if (mHaveFlashlight) {
1299            if (!mFlashlightOnPath.isEmpty()) {
1300                if (DEBUG) Log.d(TAG, "Drawing flashlight path");
1301                canvas.drawPath(mFlashlightOnPath, mFlashlightOnPaint);
1302            }
1303        }
1304        if (mHaveCamera) {
1305            if (!mCameraOnPath.isEmpty()) {
1306                if (DEBUG) Log.d(TAG, "Drawing camera path");
1307                canvas.drawPath(mCameraOnPath, mCameraOnPaint);
1308            }
1309        }
1310        if (mHaveWifi) {
1311            if (!mWifiRunningPath.isEmpty()) {
1312                if (DEBUG) Log.d(TAG, "Drawing wifi path");
1313                canvas.drawPath(mWifiRunningPath, mWifiRunningPaint);
1314            }
1315        }
1316        if (!mCpuRunningPath.isEmpty()) {
1317            if (DEBUG) Log.d(TAG, "Drawing running path");
1318            canvas.drawPath(mCpuRunningPath, mCpuRunningPaint);
1319        }
1320
1321        if (mLargeMode) {
1322            if (DEBUG) Log.d(TAG, "Drawing large mode labels");
1323            Paint.Align align = mTextPaint.getTextAlign();
1324            mTextPaint.setTextAlign(textAlignLeft);  // large-mode labels always aligned to start
1325            if (mHavePhoneSignal) {
1326                canvas.drawText(mPhoneSignalLabel, textStartX,
1327                        height - mPhoneSignalOffset - mTextDescent, mTextPaint);
1328            }
1329            if (mHaveGps) {
1330                canvas.drawText(mGpsOnLabel, textStartX,
1331                        height - mGpsOnOffset - mTextDescent, mTextPaint);
1332            }
1333            if (mHaveFlashlight) {
1334                canvas.drawText(mFlashlightOnLabel, textStartX,
1335                        height - mFlashlightOnOffset - mTextDescent, mTextPaint);
1336            }
1337            if (mHaveCamera) {
1338                canvas.drawText(mCameraOnLabel, textStartX,
1339                        height - mCameraOnOffset - mTextDescent, mTextPaint);
1340            }
1341            if (mHaveWifi) {
1342                canvas.drawText(mWifiRunningLabel, textStartX,
1343                        height - mWifiRunningOffset - mTextDescent, mTextPaint);
1344            }
1345            canvas.drawText(mCpuRunningLabel, textStartX,
1346                    height - mCpuRunningOffset - mTextDescent, mTextPaint);
1347            canvas.drawText(mChargingLabel, textStartX,
1348                    height - mChargingOffset - mTextDescent, mTextPaint);
1349            canvas.drawText(mScreenOnLabel, textStartX,
1350                    height - mScreenOnOffset - mTextDescent, mTextPaint);
1351            mTextPaint.setTextAlign(align);
1352        }
1353
1354        canvas.drawLine(mLevelLeft-mThinLineWidth, mLevelTop, mLevelLeft-mThinLineWidth,
1355                mLevelBottom+(mThinLineWidth/2), mTextPaint);
1356        if (mLargeMode) {
1357            for (int i=0; i<10; i++) {
1358                int y = mLevelTop + mThinLineWidth/2 + ((mLevelBottom-mLevelTop)*i)/10;
1359                canvas.drawLine(mLevelLeft-mThinLineWidth*2-mThinLineWidth/2, y,
1360                        mLevelLeft-mThinLineWidth-mThinLineWidth/2, y, mTextPaint);
1361            }
1362        }
1363        if (DEBUG) Log.d(TAG, "Drawing max percent, origw=" + mMaxPercentLabelStringWidth
1364                + ", noww=" + (int)mTextPaint.measureText(mMaxPercentLabelString));
1365        canvas.drawText(mMaxPercentLabelString, 0, mLevelTop, mTextPaint);
1366        canvas.drawText(mMinPercentLabelString,
1367                mMaxPercentLabelStringWidth-mMinPercentLabelStringWidth,
1368                mLevelBottom - mThinLineWidth, mTextPaint);
1369        canvas.drawLine(mLevelLeft/2, mLevelBottom+mThinLineWidth, width,
1370                mLevelBottom+mThinLineWidth, mTextPaint);
1371
1372        if (mDateLabels.size() > 0) {
1373            int ytop = mLevelTop + mTextAscent;
1374            int ybottom = mLevelBottom;
1375            int lastLeft = mLevelRight;
1376            mTextPaint.setTextAlign(Paint.Align.LEFT);
1377            for (int i=mDateLabels.size()-1; i>=0; i--) {
1378                DateLabel label = mDateLabels.get(i);
1379                int left = label.x - mThinLineWidth;
1380                int x = label.x + mThinLineWidth*2;
1381                if ((x+label.width) >= lastLeft) {
1382                    x = label.x - mThinLineWidth*2 - label.width;
1383                    left = x - mThinLineWidth;
1384                    if (left >= lastLeft) {
1385                        // okay we give up.
1386                        continue;
1387                    }
1388                }
1389                if (left < mLevelLeft) {
1390                    // Won't fit on left, give up.
1391                    continue;
1392                }
1393                mDateLinePath.reset();
1394                mDateLinePath.moveTo(label.x, ytop);
1395                mDateLinePath.lineTo(label.x, ybottom);
1396                canvas.drawPath(mDateLinePath, mDateLinePaint);
1397                canvas.drawText(label.label, x, ytop - mTextAscent, mTextPaint);
1398            }
1399        }
1400    }
1401}
1402