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