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