1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15package com.android.settings.fuelgauge; 16 17import android.content.Context; 18import android.content.Intent; 19import android.content.IntentFilter; 20import android.content.res.Resources; 21import android.os.AsyncTask; 22import android.os.BatteryManager; 23import android.os.BatteryStats; 24import android.os.BatteryStats.HistoryItem; 25import android.os.Bundle; 26import android.os.SystemClock; 27import android.support.annotation.WorkerThread; 28import android.text.format.Formatter; 29import android.util.SparseIntArray; 30 31import com.android.internal.os.BatteryStatsHelper; 32import com.android.settings.Utils; 33import com.android.settings.graph.UsageView; 34import com.android.settings.overlay.FeatureFactory; 35import com.android.settingslib.R; 36import com.android.settingslib.utils.PowerUtil; 37import com.android.settingslib.utils.StringUtil; 38 39public class BatteryInfo { 40 41 public CharSequence chargeLabel; 42 public CharSequence remainingLabel; 43 public int batteryLevel; 44 public boolean discharging = true; 45 public long remainingTimeUs = 0; 46 public long averageTimeToDischarge = Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN; 47 public String batteryPercentString; 48 public String statusLabel; 49 private boolean mCharging; 50 private BatteryStats mStats; 51 private static final String LOG_TAG = "BatteryInfo"; 52 private long timePeriod; 53 54 public interface Callback { 55 void onBatteryInfoLoaded(BatteryInfo info); 56 } 57 58 public void bindHistory(final UsageView view, BatteryDataParser... parsers) { 59 final Context context = view.getContext(); 60 BatteryDataParser parser = new BatteryDataParser() { 61 SparseIntArray points = new SparseIntArray(); 62 long startTime; 63 int lastTime = -1; 64 byte lastLevel; 65 66 @Override 67 public void onParsingStarted(long startTime, long endTime) { 68 this.startTime = startTime; 69 timePeriod = endTime - startTime; 70 view.clearPaths(); 71 // Initially configure the graph for history only. 72 view.configureGraph((int) timePeriod, 100); 73 } 74 75 @Override 76 public void onDataPoint(long time, HistoryItem record) { 77 lastTime = (int) time; 78 lastLevel = record.batteryLevel; 79 points.put(lastTime, lastLevel); 80 } 81 82 @Override 83 public void onDataGap() { 84 if (points.size() > 1) { 85 view.addPath(points); 86 } 87 points.clear(); 88 } 89 90 @Override 91 public void onParsingDone() { 92 onDataGap(); 93 94 // Add projection if we have an estimate. 95 if (remainingTimeUs != 0) { 96 PowerUsageFeatureProvider provider = FeatureFactory.getFactory(context) 97 .getPowerUsageFeatureProvider(context); 98 if (!mCharging && provider.isEnhancedBatteryPredictionEnabled(context)) { 99 points = provider.getEnhancedBatteryPredictionCurve(context, startTime); 100 } else { 101 // Linear extrapolation. 102 if (lastTime >= 0) { 103 points.put(lastTime, lastLevel); 104 points.put((int) (timePeriod + 105 PowerUtil.convertUsToMs(remainingTimeUs)), 106 mCharging ? 100 : 0); 107 } 108 } 109 } 110 111 // If we have a projection, reconfigure the graph to show it. 112 if (points != null && points.size() > 0) { 113 int maxTime = points.keyAt(points.size() - 1); 114 view.configureGraph(maxTime, 100); 115 view.addProjectedPath(points); 116 } 117 } 118 }; 119 BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1]; 120 for (int i = 0; i < parsers.length; i++) { 121 parserList[i] = parsers[i]; 122 } 123 parserList[parsers.length] = parser; 124 parse(mStats, parserList); 125 String timeString = context.getString(R.string.charge_length_format, 126 Formatter.formatShortElapsedTime(context, timePeriod)); 127 String remaining = ""; 128 if (remainingTimeUs != 0) { 129 remaining = context.getString(R.string.remaining_length_format, 130 Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000)); 131 } 132 view.setBottomLabels(new CharSequence[]{timeString, remaining}); 133 } 134 135 public static void getBatteryInfo(final Context context, final Callback callback) { 136 BatteryInfo.getBatteryInfo(context, callback, false /* shortString */); 137 } 138 139 public static void getBatteryInfo(final Context context, final Callback callback, 140 boolean shortString) { 141 final long startTime = System.currentTimeMillis(); 142 BatteryStatsHelper statsHelper = new BatteryStatsHelper(context, true); 143 statsHelper.create((Bundle) null); 144 BatteryUtils.logRuntime(LOG_TAG, "time to make batteryStatsHelper", startTime); 145 BatteryInfo.getBatteryInfo(context, callback, statsHelper, shortString); 146 } 147 148 public static void getBatteryInfo(final Context context, final Callback callback, 149 BatteryStatsHelper statsHelper, boolean shortString) { 150 final long startTime = System.currentTimeMillis(); 151 BatteryStats stats = statsHelper.getStats(); 152 BatteryUtils.logRuntime(LOG_TAG, "time for getStats", startTime); 153 getBatteryInfo(context, callback, stats, shortString); 154 } 155 156 public static void getBatteryInfo(final Context context, final Callback callback, 157 BatteryStats stats, boolean shortString) { 158 new AsyncTask<Void, Void, BatteryInfo>() { 159 @Override 160 protected BatteryInfo doInBackground(Void... params) { 161 final long startTime = System.currentTimeMillis(); 162 PowerUsageFeatureProvider provider = 163 FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context); 164 final long elapsedRealtimeUs = 165 PowerUtil.convertMsToUs(SystemClock.elapsedRealtime()); 166 167 Intent batteryBroadcast = context.registerReceiver(null, 168 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 169 // 0 means we are discharging, anything else means charging 170 boolean discharging = 171 batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0; 172 173 if (discharging && provider != null 174 && provider.isEnhancedBatteryPredictionEnabled(context)) { 175 Estimate estimate = provider.getEnhancedBatteryPrediction(context); 176 if(estimate != null) { 177 BatteryUtils 178 .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime); 179 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, 180 estimate, elapsedRealtimeUs, shortString); 181 } 182 } 183 long prediction = discharging 184 ? stats.computeBatteryTimeRemaining(elapsedRealtimeUs) : 0; 185 Estimate estimate = new Estimate( 186 PowerUtil.convertUsToMs(prediction), 187 false, /* isBasedOnUsage */ 188 Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); 189 BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime); 190 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, 191 estimate, elapsedRealtimeUs, shortString); 192 } 193 194 @Override 195 protected void onPostExecute(BatteryInfo batteryInfo) { 196 final long startTime = System.currentTimeMillis(); 197 callback.onBatteryInfoLoaded(batteryInfo); 198 BatteryUtils.logRuntime(LOG_TAG, "time for callback", startTime); 199 } 200 }.execute(); 201 } 202 203 @WorkerThread 204 public static BatteryInfo getBatteryInfoOld(Context context, Intent batteryBroadcast, 205 BatteryStats stats, long elapsedRealtimeUs, boolean shortString) { 206 Estimate estimate = new Estimate( 207 PowerUtil.convertUsToMs(stats.computeBatteryTimeRemaining(elapsedRealtimeUs)), 208 false, 209 Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); 210 return getBatteryInfo(context, batteryBroadcast, stats, estimate, elapsedRealtimeUs, 211 shortString); 212 } 213 214 @WorkerThread 215 public static BatteryInfo getBatteryInfo(Context context, Intent batteryBroadcast, 216 BatteryStats stats, Estimate estimate, long elapsedRealtimeUs, boolean shortString) { 217 final long startTime = System.currentTimeMillis(); 218 BatteryInfo info = new BatteryInfo(); 219 info.mStats = stats; 220 info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast); 221 info.batteryPercentString = Utils.formatPercentage(info.batteryLevel); 222 info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; 223 info.averageTimeToDischarge = estimate.averageDischargeTime; 224 final Resources resources = context.getResources(); 225 226 info.statusLabel = Utils.getBatteryStatus(resources, batteryBroadcast); 227 if (!info.mCharging) { 228 updateBatteryInfoDischarging(context, shortString, estimate, info); 229 } else { 230 updateBatteryInfoCharging(context, batteryBroadcast, stats, elapsedRealtimeUs, info); 231 } 232 BatteryUtils.logRuntime(LOG_TAG, "time for getBatteryInfo", startTime); 233 return info; 234 } 235 236 private static void updateBatteryInfoCharging(Context context, Intent batteryBroadcast, 237 BatteryStats stats, long elapsedRealtimeUs, BatteryInfo info) { 238 final Resources resources = context.getResources(); 239 final long chargeTime = stats.computeChargeTimeRemaining(elapsedRealtimeUs); 240 final int status = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS, 241 BatteryManager.BATTERY_STATUS_UNKNOWN); 242 info.discharging = false; 243 if (chargeTime > 0 && status != BatteryManager.BATTERY_STATUS_FULL) { 244 info.remainingTimeUs = chargeTime; 245 CharSequence timeString = StringUtil.formatElapsedTime(context, 246 PowerUtil.convertUsToMs(info.remainingTimeUs), false /* withSeconds */); 247 int resId = R.string.power_charging_duration; 248 info.remainingLabel = context.getString( 249 R.string.power_remaining_charging_duration_only, timeString); 250 info.chargeLabel = context.getString(resId, info.batteryPercentString, timeString); 251 } else { 252 final String chargeStatusLabel = resources.getString( 253 R.string.battery_info_status_charging_lower); 254 info.remainingLabel = null; 255 info.chargeLabel = info.batteryLevel == 100 ? info.batteryPercentString : 256 resources.getString(R.string.power_charging, info.batteryPercentString, 257 chargeStatusLabel); 258 } 259 } 260 261 private static void updateBatteryInfoDischarging(Context context, boolean shortString, 262 Estimate estimate, BatteryInfo info) { 263 final long drainTimeUs = PowerUtil.convertMsToUs(estimate.estimateMillis); 264 if (drainTimeUs > 0) { 265 info.remainingTimeUs = drainTimeUs; 266 info.remainingLabel = PowerUtil.getBatteryRemainingStringFormatted( 267 context, 268 PowerUtil.convertUsToMs(drainTimeUs), 269 null /* percentageString */, 270 estimate.isBasedOnUsage && !shortString 271 ); 272 info.chargeLabel = PowerUtil.getBatteryRemainingStringFormatted( 273 context, 274 PowerUtil.convertUsToMs(drainTimeUs), 275 info.batteryPercentString, 276 estimate.isBasedOnUsage && !shortString 277 ); 278 } else { 279 info.remainingLabel = null; 280 info.chargeLabel = info.batteryPercentString; 281 } 282 } 283 284 public interface BatteryDataParser { 285 void onParsingStarted(long startTime, long endTime); 286 287 void onDataPoint(long time, HistoryItem record); 288 289 void onDataGap(); 290 291 void onParsingDone(); 292 } 293 294 public static void parse(BatteryStats stats, BatteryDataParser... parsers) { 295 long startWalltime = 0; 296 long endWalltime = 0; 297 long historyStart = 0; 298 long historyEnd = 0; 299 long curWalltime = startWalltime; 300 long lastWallTime = 0; 301 long lastRealtime = 0; 302 int lastInteresting = 0; 303 int pos = 0; 304 boolean first = true; 305 if (stats.startIteratingHistoryLocked()) { 306 final HistoryItem rec = new HistoryItem(); 307 while (stats.getNextHistoryLocked(rec)) { 308 pos++; 309 if (first) { 310 first = false; 311 historyStart = rec.time; 312 } 313 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 314 || rec.cmd == HistoryItem.CMD_RESET) { 315 // If there is a ridiculously large jump in time, then we won't be 316 // able to create a good chart with that data, so just ignore the 317 // times we got before and pretend like our data extends back from 318 // the time we have now. 319 // Also, if we are getting a time change and we are less than 5 minutes 320 // since the start of the history real time, then also use this new 321 // time to compute the base time, since whatever time we had before is 322 // pretty much just noise. 323 if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L)) 324 || rec.time < (historyStart + (5 * 60 * 1000L))) { 325 startWalltime = 0; 326 } 327 lastWallTime = rec.currentTime; 328 lastRealtime = rec.time; 329 if (startWalltime == 0) { 330 startWalltime = lastWallTime - (lastRealtime - historyStart); 331 } 332 } 333 if (rec.isDeltaData()) { 334 lastInteresting = pos; 335 historyEnd = rec.time; 336 } 337 } 338 } 339 stats.finishIteratingHistoryLocked(); 340 endWalltime = lastWallTime + historyEnd - lastRealtime; 341 342 int i = 0; 343 final int N = lastInteresting; 344 345 for (int j = 0; j < parsers.length; j++) { 346 parsers[j].onParsingStarted(startWalltime, endWalltime); 347 } 348 if (endWalltime > startWalltime && stats.startIteratingHistoryLocked()) { 349 final HistoryItem rec = new HistoryItem(); 350 while (stats.getNextHistoryLocked(rec) && i < N) { 351 if (rec.isDeltaData()) { 352 curWalltime += rec.time - lastRealtime; 353 lastRealtime = rec.time; 354 long x = (curWalltime - startWalltime); 355 if (x < 0) { 356 x = 0; 357 } 358 for (int j = 0; j < parsers.length; j++) { 359 parsers[j].onDataPoint(x, rec); 360 } 361 } else { 362 long lastWalltime = curWalltime; 363 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 364 || rec.cmd == HistoryItem.CMD_RESET) { 365 if (rec.currentTime >= startWalltime) { 366 curWalltime = rec.currentTime; 367 } else { 368 curWalltime = startWalltime + (rec.time - historyStart); 369 } 370 lastRealtime = rec.time; 371 } 372 373 if (rec.cmd != HistoryItem.CMD_OVERFLOW 374 && (rec.cmd != HistoryItem.CMD_CURRENT_TIME 375 || Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) { 376 for (int j = 0; j < parsers.length; j++) { 377 parsers[j].onDataGap(); 378 } 379 } 380 } 381 i++; 382 } 383 } 384 385 stats.finishIteratingHistoryLocked(); 386 387 for (int j = 0; j < parsers.length; j++) { 388 parsers[j].onParsingDone(); 389 } 390 } 391} 392