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