/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.fuelgauge; import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Typeface; import android.os.BatteryStats; import android.os.BatteryStats.HistoryItem; import android.os.SystemClock; import android.telephony.ServiceState; import android.text.TextPaint; import android.text.format.DateFormat; import android.text.format.Formatter; import android.util.AttributeSet; import android.util.Log; import android.util.TimeUtils; import android.util.TypedValue; import android.view.View; import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.BatteryInfo; import libcore.icu.LocaleData; import java.util.ArrayList; import java.util.Calendar; import java.util.Locale; public class BatteryHistoryChart extends View { static final boolean DEBUG = false; static final String TAG = "BatteryHistoryChart"; static final int CHART_DATA_X_MASK = 0x0000ffff; static final int CHART_DATA_BIN_MASK = 0xffff0000; static final int CHART_DATA_BIN_SHIFT = 16; static class ChartData { int[] mColors; Paint[] mPaints; int mNumTicks; int[] mTicks; int mLastBin; void setColors(int[] colors) { mColors = colors; mPaints = new Paint[colors.length]; for (int i=0; i<colors.length; i++) { mPaints[i] = new Paint(); mPaints[i].setColor(colors[i]); mPaints[i].setStyle(Paint.Style.FILL); } } void init(int width) { if (width > 0) { mTicks = new int[width*2]; } else { mTicks = null; } mNumTicks = 0; mLastBin = 0; } void addTick(int x, int bin) { if (bin != mLastBin && mNumTicks < mTicks.length) { mTicks[mNumTicks] = (x&CHART_DATA_X_MASK) | (bin<<CHART_DATA_BIN_SHIFT); mNumTicks++; mLastBin = bin; } } void finish(int width) { if (mLastBin != 0) { addTick(width, 0); } } void draw(Canvas canvas, int top, int height) { int lastBin=0, lastX=0; int bottom = top + height; for (int i=0; i<mNumTicks; i++) { int tick = mTicks[i]; int x = tick&CHART_DATA_X_MASK; int bin = (tick&CHART_DATA_BIN_MASK) >> CHART_DATA_BIN_SHIFT; if (lastBin != 0) { canvas.drawRect(lastX, top, x, bottom, mPaints[lastBin]); } lastBin = bin; lastX = x; } } } static final int SANS = 1; static final int SERIF = 2; static final int MONOSPACE = 3; // First value if for phone off; first value is "scanning"; following values // are battery stats signal strength buckets. static final int NUM_PHONE_SIGNALS = 7; final Paint mBatteryBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); final Paint mBatteryGoodPaint = new Paint(Paint.ANTI_ALIAS_FLAG); final Paint mBatteryWarnPaint = new Paint(Paint.ANTI_ALIAS_FLAG); final Paint mBatteryCriticalPaint = new Paint(Paint.ANTI_ALIAS_FLAG); final Paint mTimeRemainPaint = new Paint(Paint.ANTI_ALIAS_FLAG); final Paint mChargingPaint = new Paint(); final Paint mScreenOnPaint = new Paint(); final Paint mGpsOnPaint = new Paint(); final Paint mFlashlightOnPaint = new Paint(); final Paint mCameraOnPaint = new Paint(); final Paint mWifiRunningPaint = new Paint(); final Paint mCpuRunningPaint = new Paint(); final Paint mDateLinePaint = new Paint(); final ChartData mPhoneSignalChart = new ChartData(); final TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); final TextPaint mHeaderTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); final Paint mDebugRectPaint = new Paint(); final Path mBatLevelPath = new Path(); final Path mBatGoodPath = new Path(); final Path mBatWarnPath = new Path(); final Path mBatCriticalPath = new Path(); final Path mTimeRemainPath = new Path(); final Path mChargingPath = new Path(); final Path mScreenOnPath = new Path(); final Path mGpsOnPath = new Path(); final Path mFlashlightOnPath = new Path(); final Path mCameraOnPath = new Path(); final Path mWifiRunningPath = new Path(); final Path mCpuRunningPath = new Path(); final Path mDateLinePath = new Path(); BatteryStats mStats; Intent mBatteryBroadcast; long mStatsPeriod; String mMaxPercentLabelString; String mMinPercentLabelString; String mDurationString; String mChargeDurationString; String mDrainString; String mChargingLabel; String mScreenOnLabel; String mGpsOnLabel; String mCameraOnLabel; String mFlashlightOnLabel; String mWifiRunningLabel; String mCpuRunningLabel; String mPhoneSignalLabel; BatteryInfo mInfo; int mChartMinHeight; int mHeaderHeight; int mBatteryWarnLevel; int mBatteryCriticalLevel; int mTextAscent; int mTextDescent; int mHeaderTextAscent; int mHeaderTextDescent; int mMaxPercentLabelStringWidth; int mMinPercentLabelStringWidth; int mDurationStringWidth; int mChargeLabelStringWidth; int mChargeDurationStringWidth; int mDrainStringWidth; boolean mLargeMode; int mLastWidth = -1; int mLastHeight = -1; int mLineWidth; int mThinLineWidth; int mChargingOffset; int mScreenOnOffset; int mGpsOnOffset; int mFlashlightOnOffset; int mCameraOnOffset; int mWifiRunningOffset; int mCpuRunningOffset; int mPhoneSignalOffset; int mLevelOffset; int mLevelTop; int mLevelBottom; int mLevelLeft; int mLevelRight; int mNumHist; long mHistStart; long mHistDataEnd; long mHistEnd; long mStartWallTime; long mEndDataWallTime; long mEndWallTime; int mBatLow; int mBatHigh; boolean mHaveWifi; boolean mHaveGps; boolean mHavePhoneSignal; boolean mHaveCamera; boolean mHaveFlashlight; final ArrayList<TimeLabel> mTimeLabels = new ArrayList<TimeLabel>(); final ArrayList<DateLabel> mDateLabels = new ArrayList<DateLabel>(); Bitmap mBitmap; Canvas mCanvas; static class TextAttrs { ColorStateList textColor = null; int textSize = 15; int typefaceIndex = -1; int styleIndex = -1; void retrieve(Context context, TypedArray from, int index) { TypedArray appearance = null; int ap = from.getResourceId(index, -1); if (ap != -1) { appearance = context.obtainStyledAttributes(ap, com.android.internal.R.styleable.TextAppearance); } if (appearance != null) { int n = appearance.getIndexCount(); for (int i = 0; i < n; i++) { int attr = appearance.getIndex(i); switch (attr) { case com.android.internal.R.styleable.TextAppearance_textColor: textColor = appearance.getColorStateList(attr); break; case com.android.internal.R.styleable.TextAppearance_textSize: textSize = appearance.getDimensionPixelSize(attr, textSize); break; case com.android.internal.R.styleable.TextAppearance_typeface: typefaceIndex = appearance.getInt(attr, -1); break; case com.android.internal.R.styleable.TextAppearance_textStyle: styleIndex = appearance.getInt(attr, -1); break; } } appearance.recycle(); } } void apply(Context context, TextPaint paint) { paint.density = context.getResources().getDisplayMetrics().density; paint.setCompatibilityScaling( context.getResources().getCompatibilityInfo().applicationScale); paint.setColor(textColor.getDefaultColor()); paint.setTextSize(textSize); Typeface tf = null; switch (typefaceIndex) { case SANS: tf = Typeface.SANS_SERIF; break; case SERIF: tf = Typeface.SERIF; break; case MONOSPACE: tf = Typeface.MONOSPACE; break; } setTypeface(paint, tf, styleIndex); } public void setTypeface(TextPaint paint, Typeface tf, int style) { if (style > 0) { if (tf == null) { tf = Typeface.defaultFromStyle(style); } else { tf = Typeface.create(tf, style); } paint.setTypeface(tf); // now compute what (if any) algorithmic styling is needed int typefaceStyle = tf != null ? tf.getStyle() : 0; int need = style & ~typefaceStyle; paint.setFakeBoldText((need & Typeface.BOLD) != 0); paint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); } else { paint.setFakeBoldText(false); paint.setTextSkewX(0); paint.setTypeface(tf); } } } static class TimeLabel { final int x; final String label; final int width; TimeLabel(TextPaint paint, int x, Calendar cal, boolean use24hr) { this.x = x; final String bestFormat = DateFormat.getBestDateTimePattern( Locale.getDefault(), use24hr ? "km" : "ha"); label = DateFormat.format(bestFormat, cal).toString(); width = (int)paint.measureText(label); } } static class DateLabel { final int x; final String label; final int width; DateLabel(TextPaint paint, int x, Calendar cal, boolean dayFirst) { this.x = x; final String bestFormat = DateFormat.getBestDateTimePattern( Locale.getDefault(), dayFirst ? "dM" : "Md"); label = DateFormat.format(bestFormat, cal).toString(); width = (int)paint.measureText(label); } } public BatteryHistoryChart(Context context, AttributeSet attrs) { super(context, attrs); if (DEBUG) Log.d(TAG, "New BatteryHistoryChart!"); mBatteryWarnLevel = mContext.getResources().getInteger( com.android.internal.R.integer.config_lowBatteryWarningLevel); mBatteryCriticalLevel = mContext.getResources().getInteger( com.android.internal.R.integer.config_criticalBatteryWarningLevel); mThinLineWidth = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()); mBatteryBackgroundPaint.setColor(0xFF009688); mBatteryBackgroundPaint.setStyle(Paint.Style.FILL); mBatteryGoodPaint.setARGB(128, 0, 128, 0); mBatteryGoodPaint.setStyle(Paint.Style.STROKE); mBatteryWarnPaint.setARGB(128, 128, 128, 0); mBatteryWarnPaint.setStyle(Paint.Style.STROKE); mBatteryCriticalPaint.setARGB(192, 128, 0, 0); mBatteryCriticalPaint.setStyle(Paint.Style.STROKE); mTimeRemainPaint.setColor(0xFFCED7BB); mTimeRemainPaint.setStyle(Paint.Style.FILL); mChargingPaint.setStyle(Paint.Style.STROKE); mScreenOnPaint.setStyle(Paint.Style.STROKE); mGpsOnPaint.setStyle(Paint.Style.STROKE); mCameraOnPaint.setStyle(Paint.Style.STROKE); mFlashlightOnPaint.setStyle(Paint.Style.STROKE); mWifiRunningPaint.setStyle(Paint.Style.STROKE); mCpuRunningPaint.setStyle(Paint.Style.STROKE); mPhoneSignalChart.setColors(com.android.settings.Utils.BADNESS_COLORS); mDebugRectPaint.setARGB(255, 255, 0, 0); mDebugRectPaint.setStyle(Paint.Style.STROKE); mScreenOnPaint.setColor(0xFF009688); mGpsOnPaint.setColor(0xFF009688); mCameraOnPaint.setColor(0xFF009688); mFlashlightOnPaint.setColor(0xFF009688); mWifiRunningPaint.setColor(0xFF009688); mCpuRunningPaint.setColor(0xFF009688); mChargingPaint.setColor(0xFF009688); TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.BatteryHistoryChart, 0, 0); final TextAttrs mainTextAttrs = new TextAttrs(); final TextAttrs headTextAttrs = new TextAttrs(); mainTextAttrs.retrieve(context, a, R.styleable.BatteryHistoryChart_android_textAppearance); headTextAttrs.retrieve(context, a, R.styleable.BatteryHistoryChart_headerAppearance); int shadowcolor = 0; float dx=0, dy=0, r=0; int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.BatteryHistoryChart_android_shadowColor: shadowcolor = a.getInt(attr, 0); break; case R.styleable.BatteryHistoryChart_android_shadowDx: dx = a.getFloat(attr, 0); break; case R.styleable.BatteryHistoryChart_android_shadowDy: dy = a.getFloat(attr, 0); break; case R.styleable.BatteryHistoryChart_android_shadowRadius: r = a.getFloat(attr, 0); break; case R.styleable.BatteryHistoryChart_android_textColor: mainTextAttrs.textColor = a.getColorStateList(attr); headTextAttrs.textColor = a.getColorStateList(attr); break; case R.styleable.BatteryHistoryChart_android_textSize: mainTextAttrs.textSize = a.getDimensionPixelSize(attr, mainTextAttrs.textSize); headTextAttrs.textSize = a.getDimensionPixelSize(attr, headTextAttrs.textSize); break; case R.styleable.BatteryHistoryChart_android_typeface: mainTextAttrs.typefaceIndex = a.getInt(attr, mainTextAttrs.typefaceIndex); headTextAttrs.typefaceIndex = a.getInt(attr, headTextAttrs.typefaceIndex); break; case R.styleable.BatteryHistoryChart_android_textStyle: mainTextAttrs.styleIndex = a.getInt(attr, mainTextAttrs.styleIndex); headTextAttrs.styleIndex = a.getInt(attr, headTextAttrs.styleIndex); break; case R.styleable.BatteryHistoryChart_barPrimaryColor: mBatteryBackgroundPaint.setColor(a.getInt(attr, 0)); mScreenOnPaint.setColor(a.getInt(attr, 0)); mGpsOnPaint.setColor(a.getInt(attr, 0)); mCameraOnPaint.setColor(a.getInt(attr, 0)); mFlashlightOnPaint.setColor(a.getInt(attr, 0)); mWifiRunningPaint.setColor(a.getInt(attr, 0)); mCpuRunningPaint.setColor(a.getInt(attr, 0)); mChargingPaint.setColor(a.getInt(attr, 0)); break; case R.styleable.BatteryHistoryChart_barPredictionColor: mTimeRemainPaint.setColor(a.getInt(attr, 0)); break; case R.styleable.BatteryHistoryChart_chartMinHeight: mChartMinHeight = a.getDimensionPixelSize(attr, 0); break; } } a.recycle(); mainTextAttrs.apply(context, mTextPaint); headTextAttrs.apply(context, mHeaderTextPaint); mDateLinePaint.set(mTextPaint); mDateLinePaint.setStyle(Paint.Style.STROKE); int hairlineWidth = mThinLineWidth/2; if (hairlineWidth < 1) { hairlineWidth = 1; } mDateLinePaint.setStrokeWidth(hairlineWidth); mDateLinePaint.setPathEffect(new DashPathEffect(new float[] { mThinLineWidth * 2, mThinLineWidth * 2 }, 0)); if (shadowcolor != 0) { mTextPaint.setShadowLayer(r, dx, dy, shadowcolor); mHeaderTextPaint.setShadowLayer(r, dx, dy, shadowcolor); } } void setStats(BatteryStats stats, Intent broadcast) { mStats = stats; mBatteryBroadcast = broadcast; if (DEBUG) Log.d(TAG, "Setting stats..."); final long elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; long uSecTime = mStats.computeBatteryRealtime(elapsedRealtimeUs, BatteryStats.STATS_SINCE_CHARGED); mStatsPeriod = uSecTime; mChargingLabel = getContext().getString(R.string.battery_stats_charging_label); mScreenOnLabel = getContext().getString(R.string.battery_stats_screen_on_label); mGpsOnLabel = getContext().getString(R.string.battery_stats_gps_on_label); mCameraOnLabel = getContext().getString(R.string.battery_stats_camera_on_label); mFlashlightOnLabel = getContext().getString(R.string.battery_stats_flashlight_on_label); mWifiRunningLabel = getContext().getString(R.string.battery_stats_wifi_running_label); mCpuRunningLabel = getContext().getString(R.string.battery_stats_wake_lock_label); mPhoneSignalLabel = getContext().getString(R.string.battery_stats_phone_signal_label); mMaxPercentLabelString = Utils.formatPercentage(100); mMinPercentLabelString = Utils.formatPercentage(0); mInfo = BatteryInfo.getBatteryInfo(getContext(), mBatteryBroadcast, mStats, elapsedRealtimeUs); mDrainString = ""; mChargeDurationString = ""; setContentDescription(mInfo.mChargeLabelString); int pos = 0; int lastInteresting = 0; byte lastLevel = -1; mBatLow = 0; mBatHigh = 100; mStartWallTime = 0; mEndDataWallTime = 0; mEndWallTime = 0; mHistStart = 0; mHistEnd = 0; long lastWallTime = 0; long lastRealtime = 0; int aggrStates = 0; int aggrStates2 = 0; boolean first = true; if (stats.startIteratingHistoryLocked()) { final HistoryItem rec = new HistoryItem(); while (stats.getNextHistoryLocked(rec)) { pos++; if (first) { first = false; mHistStart = rec.time; } if (rec.cmd == HistoryItem.CMD_CURRENT_TIME || rec.cmd == HistoryItem.CMD_RESET) { // If there is a ridiculously large jump in time, then we won't be // able to create a good chart with that data, so just ignore the // times we got before and pretend like our data extends back from // the time we have now. // Also, if we are getting a time change and we are less than 5 minutes // since the start of the history real time, then also use this new // time to compute the base time, since whatever time we had before is // pretty much just noise. if (rec.currentTime > (lastWallTime+(180*24*60*60*1000L)) || rec.time < (mHistStart+(5*60*1000L))) { mStartWallTime = 0; } lastWallTime = rec.currentTime; lastRealtime = rec.time; if (mStartWallTime == 0) { mStartWallTime = lastWallTime - (lastRealtime-mHistStart); } } if (rec.isDeltaData()) { if (rec.batteryLevel != lastLevel || pos == 1) { lastLevel = rec.batteryLevel; } lastInteresting = pos; mHistDataEnd = rec.time; aggrStates |= rec.states; aggrStates2 |= rec.states2; } } } mHistEnd = mHistDataEnd + (mInfo.remainingTimeUs/1000); mEndDataWallTime = lastWallTime + mHistDataEnd - lastRealtime; mEndWallTime = mEndDataWallTime + (mInfo.remainingTimeUs/1000); mNumHist = lastInteresting; mHaveGps = (aggrStates&HistoryItem.STATE_GPS_ON_FLAG) != 0; mHaveFlashlight = (aggrStates2&HistoryItem.STATE2_FLASHLIGHT_FLAG) != 0; mHaveCamera = (aggrStates2&HistoryItem.STATE2_CAMERA_FLAG) != 0; mHaveWifi = (aggrStates2&HistoryItem.STATE2_WIFI_RUNNING_FLAG) != 0 || (aggrStates&(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG |HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG |HistoryItem.STATE_WIFI_SCAN_FLAG)) != 0; if (!com.android.settings.Utils.isWifiOnly(getContext())) { mHavePhoneSignal = true; } if (mHistEnd <= mHistStart) mHistEnd = mHistStart+1; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mMaxPercentLabelStringWidth = (int)mTextPaint.measureText(mMaxPercentLabelString); mMinPercentLabelStringWidth = (int)mTextPaint.measureText(mMinPercentLabelString); mDrainStringWidth = (int)mHeaderTextPaint.measureText(mDrainString); mChargeLabelStringWidth = (int)mHeaderTextPaint.measureText(mInfo.mChargeLabelString); mChargeDurationStringWidth = (int)mHeaderTextPaint.measureText(mChargeDurationString); mTextAscent = (int)mTextPaint.ascent(); mTextDescent = (int)mTextPaint.descent(); mHeaderTextAscent = (int)mHeaderTextPaint.ascent(); mHeaderTextDescent = (int)mHeaderTextPaint.descent(); int headerTextHeight = mHeaderTextDescent - mHeaderTextAscent; mHeaderHeight = headerTextHeight*2 - mTextAscent; setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(mChartMinHeight+mHeaderHeight, heightMeasureSpec)); } void 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) { if (curLevelPath != null) { if (lastX >= 0 && lastX < w) { if (lastPath != null) { lastPath.lineTo(w, y); } curLevelPath.lineTo(w, y); } curLevelPath.lineTo(w, mLevelTop+levelh); curLevelPath.lineTo(startX, mLevelTop+levelh); curLevelPath.close(); } if (lastCharging) { mChargingPath.lineTo(w, h-mChargingOffset); } if (lastScreenOn) { mScreenOnPath.lineTo(w, h-mScreenOnOffset); } if (lastGpsOn) { mGpsOnPath.lineTo(w, h-mGpsOnOffset); } if (lastFlashlightOn) { mFlashlightOnPath.lineTo(w, h-mFlashlightOnOffset); } if (lastCameraOn) { mCameraOnPath.lineTo(w, h-mCameraOnOffset); } if (lastWifiRunning) { mWifiRunningPath.lineTo(w, h-mWifiRunningOffset); } if (lastCpuRunning) { mCpuRunningPath.lineTo(w, h - mCpuRunningOffset); } if (mHavePhoneSignal) { mPhoneSignalChart.finish(w); } } private boolean is24Hour() { return DateFormat.is24HourFormat(getContext()); } private boolean isDayFirst() { final String value = LocaleData.get(getResources().getConfiguration().locale) .getDateFormat(java.text.DateFormat.SHORT); return value.indexOf('M') > value.indexOf('d'); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (DEBUG) Log.d(TAG, "onSizeChanged: " + oldw + "x" + oldh + " to " + w + "x" + h); if (mLastWidth == w && mLastHeight == h) { return; } if (mLastWidth == 0 || mLastHeight == 0) { return; } if (DEBUG) Log.d(TAG, "Rebuilding chart for: " + w + "x" + h); mLastWidth = w; mLastHeight = h; mBitmap = null; mCanvas = null; int textHeight = mTextDescent - mTextAscent; if (h > ((textHeight*10)+mChartMinHeight)) { mLargeMode = true; if (h > (textHeight*15)) { // Plenty of room for the chart. mLineWidth = textHeight/2; } else { // Compress lines to make more room for chart. mLineWidth = textHeight/3; } } else { mLargeMode = false; mLineWidth = mThinLineWidth; } if (mLineWidth <= 0) mLineWidth = 1; mLevelTop = mHeaderHeight; mLevelLeft = mMaxPercentLabelStringWidth + mThinLineWidth*3; mLevelRight = w; int levelWidth = mLevelRight-mLevelLeft; mTextPaint.setStrokeWidth(mThinLineWidth); mBatteryGoodPaint.setStrokeWidth(mThinLineWidth); mBatteryWarnPaint.setStrokeWidth(mThinLineWidth); mBatteryCriticalPaint.setStrokeWidth(mThinLineWidth); mChargingPaint.setStrokeWidth(mLineWidth); mScreenOnPaint.setStrokeWidth(mLineWidth); mGpsOnPaint.setStrokeWidth(mLineWidth); mCameraOnPaint.setStrokeWidth(mLineWidth); mFlashlightOnPaint.setStrokeWidth(mLineWidth); mWifiRunningPaint.setStrokeWidth(mLineWidth); mCpuRunningPaint.setStrokeWidth(mLineWidth); mDebugRectPaint.setStrokeWidth(1); int fullBarOffset = textHeight + mLineWidth; if (mLargeMode) { mChargingOffset = mLineWidth; mScreenOnOffset = mChargingOffset + fullBarOffset; mCpuRunningOffset = mScreenOnOffset + fullBarOffset; mWifiRunningOffset = mCpuRunningOffset + fullBarOffset; mGpsOnOffset = mWifiRunningOffset + (mHaveWifi ? fullBarOffset : 0); mFlashlightOnOffset = mGpsOnOffset + (mHaveGps ? fullBarOffset : 0); mCameraOnOffset = mFlashlightOnOffset + (mHaveFlashlight ? fullBarOffset : 0); mPhoneSignalOffset = mCameraOnOffset + (mHaveCamera ? fullBarOffset : 0); mLevelOffset = mPhoneSignalOffset + (mHavePhoneSignal ? fullBarOffset : 0) + mLineWidth*2 + mLineWidth/2; if (mHavePhoneSignal) { mPhoneSignalChart.init(w); } } else { mScreenOnOffset = mGpsOnOffset = mCameraOnOffset = mFlashlightOnOffset = mWifiRunningOffset = mCpuRunningOffset = mChargingOffset = mPhoneSignalOffset = 0; mLevelOffset = fullBarOffset + mThinLineWidth*4; if (mHavePhoneSignal) { mPhoneSignalChart.init(0); } } mBatLevelPath.reset(); mBatGoodPath.reset(); mBatWarnPath.reset(); mTimeRemainPath.reset(); mBatCriticalPath.reset(); mScreenOnPath.reset(); mGpsOnPath.reset(); mFlashlightOnPath.reset(); mCameraOnPath.reset(); mWifiRunningPath.reset(); mCpuRunningPath.reset(); mChargingPath.reset(); mTimeLabels.clear(); mDateLabels.clear(); final long walltimeStart = mStartWallTime; final long walltimeChange = mEndWallTime > walltimeStart ? (mEndWallTime-walltimeStart) : 1; long curWalltime = mStartWallTime; long lastRealtime = 0; final int batLow = mBatLow; final int batChange = mBatHigh-mBatLow; final int levelh = h - mLevelOffset - mLevelTop; mLevelBottom = mLevelTop + levelh; int x = mLevelLeft, y = 0, startX = mLevelLeft, lastX = -1, lastY = -1; int i = 0; Path curLevelPath = null; Path lastLinePath = null; boolean lastCharging = false, lastScreenOn = false, lastGpsOn = false; boolean lastFlashlightOn = false, lastCameraOn = false; boolean lastWifiRunning = false, lastWifiSupplRunning = false, lastCpuRunning = false; int lastWifiSupplState = BatteryStats.WIFI_SUPPL_STATE_INVALID; final int N = mNumHist; if (mEndDataWallTime > mStartWallTime && mStats.startIteratingHistoryLocked()) { final HistoryItem rec = new HistoryItem(); while (mStats.getNextHistoryLocked(rec) && i < N) { if (rec.isDeltaData()) { curWalltime += rec.time-lastRealtime; lastRealtime = rec.time; x = mLevelLeft + (int)(((curWalltime-walltimeStart)*levelWidth)/walltimeChange); if (x < 0) { x = 0; } if (false) { StringBuilder sb = new StringBuilder(128); sb.append("walloff="); TimeUtils.formatDuration(curWalltime - walltimeStart, sb); sb.append(" wallchange="); TimeUtils.formatDuration(walltimeChange, sb); sb.append(" x="); sb.append(x); Log.d("foo", sb.toString()); } y = mLevelTop + levelh - ((rec.batteryLevel-batLow)*(levelh-1))/batChange; if (lastX != x) { // We have moved by at least a pixel. if (lastY != y) { // Don't plot changes within a pixel. Path path; byte value = rec.batteryLevel; if (value <= mBatteryCriticalLevel) path = mBatCriticalPath; else if (value <= mBatteryWarnLevel) path = mBatWarnPath; else path = null; //mBatGoodPath; if (path != lastLinePath) { if (lastLinePath != null) { lastLinePath.lineTo(x, y); } if (path != null) { path.moveTo(x, y); } lastLinePath = path; } else if (path != null) { path.lineTo(x, y); } if (curLevelPath == null) { curLevelPath = mBatLevelPath; curLevelPath.moveTo(x, y); startX = x; } else { curLevelPath.lineTo(x, y); } lastX = x; lastY = y; } } if (mLargeMode) { final boolean charging = (rec.states&HistoryItem.STATE_BATTERY_PLUGGED_FLAG) != 0; if (charging != lastCharging) { if (charging) { mChargingPath.moveTo(x, h-mChargingOffset); } else { mChargingPath.lineTo(x, h-mChargingOffset); } lastCharging = charging; } final boolean screenOn = (rec.states&HistoryItem.STATE_SCREEN_ON_FLAG) != 0; if (screenOn != lastScreenOn) { if (screenOn) { mScreenOnPath.moveTo(x, h-mScreenOnOffset); } else { mScreenOnPath.lineTo(x, h-mScreenOnOffset); } lastScreenOn = screenOn; } final boolean gpsOn = (rec.states&HistoryItem.STATE_GPS_ON_FLAG) != 0; if (gpsOn != lastGpsOn) { if (gpsOn) { mGpsOnPath.moveTo(x, h-mGpsOnOffset); } else { mGpsOnPath.lineTo(x, h-mGpsOnOffset); } lastGpsOn = gpsOn; } final boolean flashlightOn = (rec.states2&HistoryItem.STATE2_FLASHLIGHT_FLAG) != 0; if (flashlightOn != lastFlashlightOn) { if (flashlightOn) { mFlashlightOnPath.moveTo(x, h-mFlashlightOnOffset); } else { mFlashlightOnPath.lineTo(x, h-mFlashlightOnOffset); } lastFlashlightOn = flashlightOn; } final boolean cameraOn = (rec.states2&HistoryItem.STATE2_CAMERA_FLAG) != 0; if (cameraOn != lastCameraOn) { if (cameraOn) { mCameraOnPath.moveTo(x, h-mCameraOnOffset); } else { mCameraOnPath.lineTo(x, h-mCameraOnOffset); } lastCameraOn = cameraOn; } final int wifiSupplState = ((rec.states2&HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK) >> HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT); boolean wifiRunning; if (lastWifiSupplState != wifiSupplState) { lastWifiSupplState = wifiSupplState; switch (wifiSupplState) { case BatteryStats.WIFI_SUPPL_STATE_DISCONNECTED: case BatteryStats.WIFI_SUPPL_STATE_DORMANT: case BatteryStats.WIFI_SUPPL_STATE_INACTIVE: case BatteryStats.WIFI_SUPPL_STATE_INTERFACE_DISABLED: case BatteryStats.WIFI_SUPPL_STATE_INVALID: case BatteryStats.WIFI_SUPPL_STATE_UNINITIALIZED: wifiRunning = lastWifiSupplRunning = false; break; default: wifiRunning = lastWifiSupplRunning = true; break; } } else { wifiRunning = lastWifiSupplRunning; } if ((rec.states&(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG |HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG |HistoryItem.STATE_WIFI_SCAN_FLAG)) != 0) { wifiRunning = true; } if (wifiRunning != lastWifiRunning) { if (wifiRunning) { mWifiRunningPath.moveTo(x, h-mWifiRunningOffset); } else { mWifiRunningPath.lineTo(x, h-mWifiRunningOffset); } lastWifiRunning = wifiRunning; } final boolean cpuRunning = (rec.states&HistoryItem.STATE_CPU_RUNNING_FLAG) != 0; if (cpuRunning != lastCpuRunning) { if (cpuRunning) { mCpuRunningPath.moveTo(x, h - mCpuRunningOffset); } else { mCpuRunningPath.lineTo(x, h - mCpuRunningOffset); } lastCpuRunning = cpuRunning; } if (mLargeMode && mHavePhoneSignal) { int bin; if (((rec.states&HistoryItem.STATE_PHONE_STATE_MASK) >> HistoryItem.STATE_PHONE_STATE_SHIFT) == ServiceState.STATE_POWER_OFF) { bin = 0; } else if ((rec.states&HistoryItem.STATE_PHONE_SCANNING_FLAG) != 0) { bin = 1; } else { bin = (rec.states&HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK) >> HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT; bin += 2; } mPhoneSignalChart.addTick(x, bin); } } } else { long lastWalltime = curWalltime; if (rec.cmd == HistoryItem.CMD_CURRENT_TIME || rec.cmd == HistoryItem.CMD_RESET) { if (rec.currentTime >= mStartWallTime) { curWalltime = rec.currentTime; } else { curWalltime = mStartWallTime + (rec.time-mHistStart); } lastRealtime = rec.time; } if (rec.cmd != HistoryItem.CMD_OVERFLOW && (rec.cmd != HistoryItem.CMD_CURRENT_TIME || Math.abs(lastWalltime-curWalltime) > (60*60*1000))) { if (curLevelPath != null) { finishPaths(x+1, h, levelh, startX, lastY, curLevelPath, lastX, lastCharging, lastScreenOn, lastGpsOn, lastFlashlightOn, lastCameraOn, lastWifiRunning, lastCpuRunning, lastLinePath); lastX = lastY = -1; curLevelPath = null; lastLinePath = null; lastCharging = lastScreenOn = lastGpsOn = lastFlashlightOn = lastCameraOn = lastCpuRunning = false; } } } i++; } mStats.finishIteratingHistoryLocked(); } if (lastY < 0 || lastX < 0) { // Didn't get any data... x = lastX = mLevelLeft; y = lastY = mLevelTop + levelh - ((mInfo.mBatteryLevel-batLow)*(levelh-1))/batChange; Path path; byte value = (byte)mInfo.mBatteryLevel; if (value <= mBatteryCriticalLevel) path = mBatCriticalPath; else if (value <= mBatteryWarnLevel) path = mBatWarnPath; else path = null; //mBatGoodPath; if (path != null) { path.moveTo(x, y); lastLinePath = path; } mBatLevelPath.moveTo(x, y); curLevelPath = mBatLevelPath; x = w; } else { // Figure out where the actual data ends on the screen. x = mLevelLeft + (int)(((mEndDataWallTime-walltimeStart)*levelWidth)/walltimeChange); if (x < 0) { x = 0; } } finishPaths(x, h, levelh, startX, lastY, curLevelPath, lastX, lastCharging, lastScreenOn, lastGpsOn, lastFlashlightOn, lastCameraOn, lastWifiRunning, lastCpuRunning, lastLinePath); if (x < w) { // If we reserved room for the remaining time, create a final path to draw // that part of the UI. mTimeRemainPath.moveTo(x, lastY); int fullY = mLevelTop + levelh - ((100-batLow)*(levelh-1))/batChange; int emptyY = mLevelTop + levelh - ((0-batLow)*(levelh-1))/batChange; if (mInfo.mDischarging) { mTimeRemainPath.lineTo(mLevelRight, emptyY); } else { mTimeRemainPath.lineTo(mLevelRight, fullY); mTimeRemainPath.lineTo(mLevelRight, emptyY); } mTimeRemainPath.lineTo(x, emptyY); mTimeRemainPath.close(); } if (mStartWallTime > 0 && mEndWallTime > mStartWallTime) { // Create the time labels at the bottom. boolean is24hr = is24Hour(); Calendar calStart = Calendar.getInstance(); calStart.setTimeInMillis(mStartWallTime); calStart.set(Calendar.MILLISECOND, 0); calStart.set(Calendar.SECOND, 0); calStart.set(Calendar.MINUTE, 0); long startRoundTime = calStart.getTimeInMillis(); if (startRoundTime < mStartWallTime) { calStart.set(Calendar.HOUR_OF_DAY, calStart.get(Calendar.HOUR_OF_DAY)+1); startRoundTime = calStart.getTimeInMillis(); } Calendar calEnd = Calendar.getInstance(); calEnd.setTimeInMillis(mEndWallTime); calEnd.set(Calendar.MILLISECOND, 0); calEnd.set(Calendar.SECOND, 0); calEnd.set(Calendar.MINUTE, 0); long endRoundTime = calEnd.getTimeInMillis(); if (startRoundTime < endRoundTime) { addTimeLabel(calStart, mLevelLeft, mLevelRight, is24hr); Calendar calMid = Calendar.getInstance(); calMid.setTimeInMillis(mStartWallTime+((mEndWallTime-mStartWallTime)/2)); calMid.set(Calendar.MILLISECOND, 0); calMid.set(Calendar.SECOND, 0); calMid.set(Calendar.MINUTE, 0); long calMidMillis = calMid.getTimeInMillis(); if (calMidMillis > startRoundTime && calMidMillis < endRoundTime) { addTimeLabel(calMid, mLevelLeft, mLevelRight, is24hr); } addTimeLabel(calEnd, mLevelLeft, mLevelRight, is24hr); } // Create the date labels if the chart includes multiple days if (calStart.get(Calendar.DAY_OF_YEAR) != calEnd.get(Calendar.DAY_OF_YEAR) || calStart.get(Calendar.YEAR) != calEnd.get(Calendar.YEAR)) { boolean isDayFirst = isDayFirst(); calStart.set(Calendar.HOUR_OF_DAY, 0); startRoundTime = calStart.getTimeInMillis(); if (startRoundTime < mStartWallTime) { calStart.set(Calendar.DAY_OF_YEAR, calStart.get(Calendar.DAY_OF_YEAR) + 1); startRoundTime = calStart.getTimeInMillis(); } calEnd.set(Calendar.HOUR_OF_DAY, 0); endRoundTime = calEnd.getTimeInMillis(); if (startRoundTime < endRoundTime) { addDateLabel(calStart, mLevelLeft, mLevelRight, isDayFirst); Calendar calMid = Calendar.getInstance(); // The middle between two beginnings of days can be anywhere between -1 to 13 // after the beginning of the "median" day. calMid.setTimeInMillis(startRoundTime + ((endRoundTime - startRoundTime) / 2) + 2 * 60 * 60 * 1000); calMid.set(Calendar.HOUR_OF_DAY, 0); calMid.set(Calendar.MINUTE, 0); long calMidMillis = calMid.getTimeInMillis(); if (calMidMillis > startRoundTime && calMidMillis < endRoundTime) { addDateLabel(calMid, mLevelLeft, mLevelRight, isDayFirst); } } addDateLabel(calEnd, mLevelLeft, mLevelRight, isDayFirst); } } if (mTimeLabels.size() < 2) { // If there are fewer than 2 time labels, then they are useless. Just // show an axis label giving the entire duration. mDurationString = Formatter.formatShortElapsedTime(getContext(), mEndWallTime - mStartWallTime); mDurationStringWidth = (int)mTextPaint.measureText(mDurationString); } else { mDurationString = null; mDurationStringWidth = 0; } } void addTimeLabel(Calendar cal, int levelLeft, int levelRight, boolean is24hr) { final long walltimeStart = mStartWallTime; final long walltimeChange = mEndWallTime-walltimeStart; mTimeLabels.add(new TimeLabel(mTextPaint, levelLeft + (int)(((cal.getTimeInMillis()-walltimeStart)*(levelRight-levelLeft)) / walltimeChange), cal, is24hr)); } void addDateLabel(Calendar cal, int levelLeft, int levelRight, boolean isDayFirst) { final long walltimeStart = mStartWallTime; final long walltimeChange = mEndWallTime-walltimeStart; mDateLabels.add(new DateLabel(mTextPaint, levelLeft + (int)(((cal.getTimeInMillis()-walltimeStart)*(levelRight-levelLeft)) / walltimeChange), cal, isDayFirst)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int width = getWidth(); final int height = getHeight(); //buildBitmap(width, height); if (DEBUG) Log.d(TAG, "onDraw: " + width + "x" + height); //canvas.drawBitmap(mBitmap, 0, 0, null); drawChart(canvas, width, height); } void buildBitmap(int width, int height) { if (mBitmap != null && width == mBitmap.getWidth() && height == mBitmap.getHeight()) { return; } if (DEBUG) Log.d(TAG, "buildBitmap: " + width + "x" + height); mBitmap = Bitmap.createBitmap(getResources().getDisplayMetrics(), width, height, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBitmap); drawChart(mCanvas, width, height); } void drawChart(Canvas canvas, int width, int height) { final boolean layoutRtl = isLayoutRtl(); final int textStartX = layoutRtl ? width : 0; final int textEndX = layoutRtl ? 0 : width; final Paint.Align textAlignLeft = layoutRtl ? Paint.Align.RIGHT : Paint.Align.LEFT; final Paint.Align textAlignRight = layoutRtl ? Paint.Align.LEFT : Paint.Align.RIGHT; if (DEBUG) { canvas.drawRect(1, 1, width, height, mDebugRectPaint); } if (DEBUG) Log.d(TAG, "Drawing level path."); canvas.drawPath(mBatLevelPath, mBatteryBackgroundPaint); if (!mTimeRemainPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing time remain path."); canvas.drawPath(mTimeRemainPath, mTimeRemainPaint); } if (mTimeLabels.size() > 1) { int y = mLevelBottom - mTextAscent + (mThinLineWidth*4); int ytick = mLevelBottom+mThinLineWidth+(mThinLineWidth/2); mTextPaint.setTextAlign(Paint.Align.LEFT); int lastX = 0; for (int i=0; i<mTimeLabels.size(); i++) { TimeLabel label = mTimeLabels.get(i); if (i == 0) { int x = label.x - label.width/2; if (x < 0) { x = 0; } if (DEBUG) Log.d(TAG, "Drawing left label: " + label.label + " @ " + x); canvas.drawText(label.label, x, y, mTextPaint); canvas.drawLine(label.x, ytick, label.x, ytick+mThinLineWidth, mTextPaint); lastX = x + label.width; } else if (i < (mTimeLabels.size()-1)) { int x = label.x - label.width/2; if (x < (lastX+mTextAscent)) { continue; } TimeLabel nextLabel = mTimeLabels.get(i+1); if (x > (width-nextLabel.width-mTextAscent)) { continue; } if (DEBUG) Log.d(TAG, "Drawing middle label: " + label.label + " @ " + x); canvas.drawText(label.label, x, y, mTextPaint); canvas.drawLine(label.x, ytick, label.x, ytick + mThinLineWidth, mTextPaint); lastX = x + label.width; } else { int x = label.x - label.width/2; if ((x+label.width) >= width) { x = width-1-label.width; } if (DEBUG) Log.d(TAG, "Drawing right label: " + label.label + " @ " + x); canvas.drawText(label.label, x, y, mTextPaint); canvas.drawLine(label.x, ytick, label.x, ytick+mThinLineWidth, mTextPaint); } } } else if (mDurationString != null) { int y = mLevelBottom - mTextAscent + (mThinLineWidth*4); mTextPaint.setTextAlign(Paint.Align.LEFT); canvas.drawText(mDurationString, mLevelLeft + (mLevelRight-mLevelLeft)/2 - mDurationStringWidth/2, y, mTextPaint); } int headerTop = -mHeaderTextAscent + (mHeaderTextDescent-mHeaderTextAscent)/3; mHeaderTextPaint.setTextAlign(textAlignLeft); if (DEBUG) Log.d(TAG, "Drawing charge label string: " + mInfo.mChargeLabelString); canvas.drawText(mInfo.mChargeLabelString, textStartX, headerTop, mHeaderTextPaint); int stringHalfWidth = mChargeDurationStringWidth / 2; if (layoutRtl) stringHalfWidth = -stringHalfWidth; int headerCenter = ((width-mChargeDurationStringWidth-mDrainStringWidth)/2) + (layoutRtl ? mDrainStringWidth : mChargeLabelStringWidth); if (DEBUG) Log.d(TAG, "Drawing charge duration string: " + mChargeDurationString); canvas.drawText(mChargeDurationString, headerCenter - stringHalfWidth, headerTop, mHeaderTextPaint); mHeaderTextPaint.setTextAlign(textAlignRight); if (DEBUG) Log.d(TAG, "Drawing drain string: " + mDrainString); canvas.drawText(mDrainString, textEndX, headerTop, mHeaderTextPaint); if (!mBatGoodPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing good battery path"); canvas.drawPath(mBatGoodPath, mBatteryGoodPaint); } if (!mBatWarnPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing warn battery path"); canvas.drawPath(mBatWarnPath, mBatteryWarnPaint); } if (!mBatCriticalPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing critical battery path"); canvas.drawPath(mBatCriticalPath, mBatteryCriticalPaint); } if (mHavePhoneSignal) { if (DEBUG) Log.d(TAG, "Drawing phone signal path"); int top = height-mPhoneSignalOffset - (mLineWidth/2); mPhoneSignalChart.draw(canvas, top, mLineWidth); } if (!mScreenOnPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing screen on path"); canvas.drawPath(mScreenOnPath, mScreenOnPaint); } if (!mChargingPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing charging path"); canvas.drawPath(mChargingPath, mChargingPaint); } if (mHaveGps) { if (!mGpsOnPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing gps path"); canvas.drawPath(mGpsOnPath, mGpsOnPaint); } } if (mHaveFlashlight) { if (!mFlashlightOnPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing flashlight path"); canvas.drawPath(mFlashlightOnPath, mFlashlightOnPaint); } } if (mHaveCamera) { if (!mCameraOnPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing camera path"); canvas.drawPath(mCameraOnPath, mCameraOnPaint); } } if (mHaveWifi) { if (!mWifiRunningPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing wifi path"); canvas.drawPath(mWifiRunningPath, mWifiRunningPaint); } } if (!mCpuRunningPath.isEmpty()) { if (DEBUG) Log.d(TAG, "Drawing running path"); canvas.drawPath(mCpuRunningPath, mCpuRunningPaint); } if (mLargeMode) { if (DEBUG) Log.d(TAG, "Drawing large mode labels"); Paint.Align align = mTextPaint.getTextAlign(); mTextPaint.setTextAlign(textAlignLeft); // large-mode labels always aligned to start if (mHavePhoneSignal) { canvas.drawText(mPhoneSignalLabel, textStartX, height - mPhoneSignalOffset - mTextDescent, mTextPaint); } if (mHaveGps) { canvas.drawText(mGpsOnLabel, textStartX, height - mGpsOnOffset - mTextDescent, mTextPaint); } if (mHaveFlashlight) { canvas.drawText(mFlashlightOnLabel, textStartX, height - mFlashlightOnOffset - mTextDescent, mTextPaint); } if (mHaveCamera) { canvas.drawText(mCameraOnLabel, textStartX, height - mCameraOnOffset - mTextDescent, mTextPaint); } if (mHaveWifi) { canvas.drawText(mWifiRunningLabel, textStartX, height - mWifiRunningOffset - mTextDescent, mTextPaint); } canvas.drawText(mCpuRunningLabel, textStartX, height - mCpuRunningOffset - mTextDescent, mTextPaint); canvas.drawText(mChargingLabel, textStartX, height - mChargingOffset - mTextDescent, mTextPaint); canvas.drawText(mScreenOnLabel, textStartX, height - mScreenOnOffset - mTextDescent, mTextPaint); mTextPaint.setTextAlign(align); } canvas.drawLine(mLevelLeft-mThinLineWidth, mLevelTop, mLevelLeft-mThinLineWidth, mLevelBottom+(mThinLineWidth/2), mTextPaint); if (mLargeMode) { for (int i=0; i<10; i++) { int y = mLevelTop + mThinLineWidth/2 + ((mLevelBottom-mLevelTop)*i)/10; canvas.drawLine(mLevelLeft-mThinLineWidth*2-mThinLineWidth/2, y, mLevelLeft-mThinLineWidth-mThinLineWidth/2, y, mTextPaint); } } if (DEBUG) Log.d(TAG, "Drawing max percent, origw=" + mMaxPercentLabelStringWidth + ", noww=" + (int)mTextPaint.measureText(mMaxPercentLabelString)); canvas.drawText(mMaxPercentLabelString, 0, mLevelTop, mTextPaint); canvas.drawText(mMinPercentLabelString, mMaxPercentLabelStringWidth-mMinPercentLabelStringWidth, mLevelBottom - mThinLineWidth, mTextPaint); canvas.drawLine(mLevelLeft/2, mLevelBottom+mThinLineWidth, width, mLevelBottom+mThinLineWidth, mTextPaint); if (mDateLabels.size() > 0) { int ytop = mLevelTop + mTextAscent; int ybottom = mLevelBottom; int lastLeft = mLevelRight; mTextPaint.setTextAlign(Paint.Align.LEFT); for (int i=mDateLabels.size()-1; i>=0; i--) { DateLabel label = mDateLabels.get(i); int left = label.x - mThinLineWidth; int x = label.x + mThinLineWidth*2; if ((x+label.width) >= lastLeft) { x = label.x - mThinLineWidth*2 - label.width; left = x - mThinLineWidth; if (left >= lastLeft) { // okay we give up. continue; } } if (left < mLevelLeft) { // Won't fit on left, give up. continue; } mDateLinePath.reset(); mDateLinePath.moveTo(label.x, ytop); mDateLinePath.lineTo(label.x, ybottom); canvas.drawPath(mDateLinePath, mDateLinePaint); canvas.drawText(label.label, x, ytop - mTextAscent, mTextPaint); } } } }