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.HistoryItem; 24 import android.os.BatteryStatsManager; 25 import android.os.BatteryUsageStats; 26 import android.os.SystemClock; 27 import android.text.format.Formatter; 28 import android.util.Log; 29 import android.util.SparseIntArray; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.WorkerThread; 34 35 import com.android.internal.os.BatteryStatsHistoryIterator; 36 import com.android.settings.Utils; 37 import com.android.settings.overlay.FeatureFactory; 38 import com.android.settings.widget.UsageView; 39 import com.android.settingslib.R; 40 import com.android.settingslib.fuelgauge.Estimate; 41 import com.android.settingslib.fuelgauge.EstimateKt; 42 import com.android.settingslib.utils.PowerUtil; 43 import com.android.settingslib.utils.StringUtil; 44 45 public class BatteryInfo { 46 private static final String TAG = "BatteryInfo"; 47 48 public CharSequence chargeLabel; 49 public CharSequence remainingLabel; 50 public int batteryLevel; 51 public int batteryStatus; 52 public boolean discharging = true; 53 public boolean isOverheated; 54 public long remainingTimeUs = 0; 55 public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN; 56 public String batteryPercentString; 57 public String statusLabel; 58 public String suggestionLabel; 59 private boolean mCharging; 60 private BatteryUsageStats mBatteryUsageStats; 61 private static final String LOG_TAG = "BatteryInfo"; 62 private long timePeriod; 63 64 public interface Callback { onBatteryInfoLoaded(BatteryInfo info)65 void onBatteryInfoLoaded(BatteryInfo info); 66 } 67 bindHistory(final UsageView view, BatteryDataParser... parsers)68 public void bindHistory(final UsageView view, BatteryDataParser... parsers) { 69 final Context context = view.getContext(); 70 BatteryDataParser parser = new BatteryDataParser() { 71 SparseIntArray points = new SparseIntArray(); 72 long startTime; 73 int lastTime = -1; 74 byte lastLevel; 75 76 @Override 77 public void onParsingStarted(long startTime, long endTime) { 78 this.startTime = startTime; 79 timePeriod = endTime - startTime; 80 view.clearPaths(); 81 // Initially configure the graph for history only. 82 view.configureGraph((int) timePeriod, 100); 83 } 84 85 @Override 86 public void onDataPoint(long time, HistoryItem record) { 87 lastTime = (int) time; 88 lastLevel = record.batteryLevel; 89 points.put(lastTime, lastLevel); 90 } 91 92 @Override 93 public void onDataGap() { 94 if (points.size() > 1) { 95 view.addPath(points); 96 } 97 points.clear(); 98 } 99 100 @Override 101 public void onParsingDone() { 102 onDataGap(); 103 104 // Add projection if we have an estimate. 105 if (remainingTimeUs != 0) { 106 PowerUsageFeatureProvider provider = FeatureFactory.getFactory(context) 107 .getPowerUsageFeatureProvider(context); 108 if (!mCharging && provider.isEnhancedBatteryPredictionEnabled(context)) { 109 points = provider.getEnhancedBatteryPredictionCurve(context, startTime); 110 } else { 111 // Linear extrapolation. 112 if (lastTime >= 0) { 113 points.put(lastTime, lastLevel); 114 points.put((int) (timePeriod + 115 PowerUtil.convertUsToMs(remainingTimeUs)), 116 mCharging ? 100 : 0); 117 } 118 } 119 } 120 121 // If we have a projection, reconfigure the graph to show it. 122 if (points != null && points.size() > 0) { 123 int maxTime = points.keyAt(points.size() - 1); 124 view.configureGraph(maxTime, 100); 125 view.addProjectedPath(points); 126 } 127 } 128 }; 129 BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1]; 130 for (int i = 0; i < parsers.length; i++) { 131 parserList[i] = parsers[i]; 132 } 133 parserList[parsers.length] = parser; 134 parseBatteryHistory(parserList); 135 String timeString = context.getString(R.string.charge_length_format, 136 Formatter.formatShortElapsedTime(context, timePeriod)); 137 String remaining = ""; 138 if (remainingTimeUs != 0) { 139 remaining = context.getString(R.string.remaining_length_format, 140 Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000)); 141 } 142 view.setBottomLabels(new CharSequence[]{timeString, remaining}); 143 } 144 getBatteryInfo(final Context context, final Callback callback, boolean shortString)145 public static void getBatteryInfo(final Context context, final Callback callback, 146 boolean shortString) { 147 BatteryInfo.getBatteryInfo(context, callback, /* batteryUsageStats */ null, shortString); 148 } 149 getBatteryInfo(final Context context, final Callback callback, @Nullable final BatteryUsageStats batteryUsageStats, boolean shortString)150 public static void getBatteryInfo(final Context context, final Callback callback, 151 @Nullable final BatteryUsageStats batteryUsageStats, 152 boolean shortString) { 153 new AsyncTask<Void, Void, BatteryInfo>() { 154 @Override 155 protected BatteryInfo doInBackground(Void... params) { 156 BatteryUsageStats stats; 157 if (batteryUsageStats != null) { 158 stats = batteryUsageStats; 159 } else { 160 try { 161 stats = context.getSystemService(BatteryStatsManager.class) 162 .getBatteryUsageStats(); 163 } catch (RuntimeException e) { 164 Log.e(TAG, "getBatteryInfo() from getBatteryUsageStats()", e); 165 // Use default BatteryUsageStats. 166 stats = new BatteryUsageStats.Builder( 167 new String[0], /* includePowerModels */ false).build(); 168 } 169 } 170 return getBatteryInfo(context, stats, shortString); 171 } 172 173 @Override 174 protected void onPostExecute(BatteryInfo batteryInfo) { 175 final long startTime = System.currentTimeMillis(); 176 callback.onBatteryInfoLoaded(batteryInfo); 177 BatteryUtils.logRuntime(LOG_TAG, "time for callback", startTime); 178 } 179 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 180 } 181 182 /** 183 * Creates a BatteryInfo based on BatteryUsageStats 184 */ 185 @WorkerThread getBatteryInfo(final Context context, @NonNull final BatteryUsageStats batteryUsageStats, boolean shortString)186 public static BatteryInfo getBatteryInfo(final Context context, 187 @NonNull final BatteryUsageStats batteryUsageStats, boolean shortString) { 188 final long batteryStatsTime = System.currentTimeMillis(); 189 BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime); 190 191 final long startTime = System.currentTimeMillis(); 192 PowerUsageFeatureProvider provider = 193 FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context); 194 final long elapsedRealtimeUs = 195 PowerUtil.convertMsToUs(SystemClock.elapsedRealtime()); 196 197 final Intent batteryBroadcast = context.registerReceiver(null, 198 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 199 // 0 means we are discharging, anything else means charging 200 final boolean discharging = 201 batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0; 202 203 if (discharging && provider != null 204 && provider.isEnhancedBatteryPredictionEnabled(context)) { 205 Estimate estimate = provider.getEnhancedBatteryPrediction(context); 206 if (estimate != null) { 207 Estimate.storeCachedEstimate(context, estimate); 208 BatteryUtils 209 .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime); 210 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, batteryUsageStats, 211 estimate, elapsedRealtimeUs, shortString); 212 } 213 } 214 final long prediction = discharging ? batteryUsageStats.getBatteryTimeRemainingMs() : 0; 215 final Estimate estimate = new Estimate( 216 prediction, 217 false, /* isBasedOnUsage */ 218 EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); 219 BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime); 220 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, batteryUsageStats, 221 estimate, elapsedRealtimeUs, shortString); 222 } 223 224 @WorkerThread getBatteryInfoOld(Context context, Intent batteryBroadcast, BatteryUsageStats batteryUsageStats, long elapsedRealtimeUs, boolean shortString)225 public static BatteryInfo getBatteryInfoOld(Context context, Intent batteryBroadcast, 226 BatteryUsageStats batteryUsageStats, long elapsedRealtimeUs, boolean shortString) { 227 Estimate estimate = new Estimate( 228 batteryUsageStats.getBatteryTimeRemainingMs(), 229 false, 230 EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); 231 return getBatteryInfo(context, batteryBroadcast, batteryUsageStats, estimate, 232 elapsedRealtimeUs, shortString); 233 } 234 235 @WorkerThread getBatteryInfo(Context context, Intent batteryBroadcast, @NonNull BatteryUsageStats batteryUsageStats, Estimate estimate, long elapsedRealtimeUs, boolean shortString)236 public static BatteryInfo getBatteryInfo(Context context, Intent batteryBroadcast, 237 @NonNull BatteryUsageStats batteryUsageStats, Estimate estimate, 238 long elapsedRealtimeUs, boolean shortString) { 239 final long startTime = System.currentTimeMillis(); 240 BatteryInfo info = new BatteryInfo(); 241 info.mBatteryUsageStats = batteryUsageStats; 242 info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast); 243 info.batteryPercentString = Utils.formatPercentage(info.batteryLevel); 244 info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; 245 info.averageTimeToDischarge = estimate.getAverageDischargeTime(); 246 info.isOverheated = batteryBroadcast.getIntExtra( 247 BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN) 248 == BatteryManager.BATTERY_HEALTH_OVERHEAT; 249 250 info.statusLabel = Utils.getBatteryStatus(context, batteryBroadcast); 251 info.batteryStatus = batteryBroadcast.getIntExtra( 252 BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); 253 if (!info.mCharging) { 254 updateBatteryInfoDischarging(context, shortString, estimate, info); 255 } else { 256 updateBatteryInfoCharging(context, batteryBroadcast, batteryUsageStats, 257 info); 258 } 259 BatteryUtils.logRuntime(LOG_TAG, "time for getBatteryInfo", startTime); 260 return info; 261 } 262 updateBatteryInfoCharging(Context context, Intent batteryBroadcast, BatteryUsageStats stats, BatteryInfo info)263 private static void updateBatteryInfoCharging(Context context, Intent batteryBroadcast, 264 BatteryUsageStats stats, BatteryInfo info) { 265 final Resources resources = context.getResources(); 266 final long chargeTimeMs = stats.getChargeTimeRemainingMs(); 267 final int status = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS, 268 BatteryManager.BATTERY_STATUS_UNKNOWN); 269 info.discharging = false; 270 info.suggestionLabel = null; 271 if (info.isOverheated && status != BatteryManager.BATTERY_STATUS_FULL) { 272 info.remainingLabel = null; 273 int chargingLimitedResId = R.string.power_charging_limited; 274 info.chargeLabel = 275 context.getString(chargingLimitedResId, info.batteryPercentString); 276 } else if (chargeTimeMs > 0 && status != BatteryManager.BATTERY_STATUS_FULL) { 277 info.remainingTimeUs = PowerUtil.convertMsToUs(chargeTimeMs); 278 final CharSequence timeString = StringUtil.formatElapsedTime( 279 context, 280 PowerUtil.convertUsToMs(info.remainingTimeUs), 281 false /* withSeconds */, 282 true /* collapseTimeUnit */); 283 int resId = R.string.power_charging_duration; 284 info.remainingLabel = context.getString( 285 R.string.power_remaining_charging_duration_only, timeString); 286 info.chargeLabel = context.getString(resId, info.batteryPercentString, timeString); 287 } else { 288 final String chargeStatusLabel = Utils.getBatteryStatus(context, batteryBroadcast); 289 info.remainingLabel = null; 290 info.chargeLabel = info.batteryLevel == 100 ? info.batteryPercentString : 291 resources.getString(R.string.power_charging, info.batteryPercentString, 292 chargeStatusLabel.toLowerCase()); 293 } 294 } 295 updateBatteryInfoDischarging(Context context, boolean shortString, Estimate estimate, BatteryInfo info)296 private static void updateBatteryInfoDischarging(Context context, boolean shortString, 297 Estimate estimate, BatteryInfo info) { 298 final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis()); 299 if (drainTimeUs > 0) { 300 info.remainingTimeUs = drainTimeUs; 301 info.remainingLabel = PowerUtil.getBatteryRemainingStringFormatted( 302 context, 303 PowerUtil.convertUsToMs(drainTimeUs), 304 null /* percentageString */, 305 false /* basedOnUsage */ 306 ); 307 info.chargeLabel = PowerUtil.getBatteryRemainingStringFormatted( 308 context, 309 PowerUtil.convertUsToMs(drainTimeUs), 310 info.batteryPercentString, 311 estimate.isBasedOnUsage() && !shortString 312 ); 313 info.suggestionLabel = PowerUtil.getBatteryTipStringFormatted( 314 context, PowerUtil.convertUsToMs(drainTimeUs)); 315 } else { 316 info.remainingLabel = null; 317 info.suggestionLabel = null; 318 info.chargeLabel = info.batteryPercentString; 319 } 320 } 321 322 public interface BatteryDataParser { onParsingStarted(long startTime, long endTime)323 void onParsingStarted(long startTime, long endTime); 324 onDataPoint(long time, HistoryItem record)325 void onDataPoint(long time, HistoryItem record); 326 onDataGap()327 void onDataGap(); 328 onParsingDone()329 void onParsingDone(); 330 } 331 332 /** 333 * Iterates over battery history included in the BatteryUsageStats that this object 334 * was initialized with. 335 */ parseBatteryHistory(BatteryDataParser... parsers)336 public void parseBatteryHistory(BatteryDataParser... parsers) { 337 long startWalltime = 0; 338 long endWalltime = 0; 339 long historyStart = 0; 340 long historyEnd = 0; 341 long curWalltime = startWalltime; 342 long lastWallTime = 0; 343 long lastRealtime = 0; 344 int lastInteresting = 0; 345 int pos = 0; 346 boolean first = true; 347 final BatteryStatsHistoryIterator iterator1 = 348 mBatteryUsageStats.iterateBatteryStatsHistory(); 349 final HistoryItem rec = new HistoryItem(); 350 while (iterator1.next(rec)) { 351 pos++; 352 if (first) { 353 first = false; 354 historyStart = rec.time; 355 } 356 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 357 || rec.cmd == HistoryItem.CMD_RESET) { 358 // If there is a ridiculously large jump in time, then we won't be 359 // able to create a good chart with that data, so just ignore the 360 // times we got before and pretend like our data extends back from 361 // the time we have now. 362 // Also, if we are getting a time change and we are less than 5 minutes 363 // since the start of the history real time, then also use this new 364 // time to compute the base time, since whatever time we had before is 365 // pretty much just noise. 366 if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L)) 367 || rec.time < (historyStart + (5 * 60 * 1000L))) { 368 startWalltime = 0; 369 } 370 lastWallTime = rec.currentTime; 371 lastRealtime = rec.time; 372 if (startWalltime == 0) { 373 startWalltime = lastWallTime - (lastRealtime - historyStart); 374 } 375 } 376 if (rec.isDeltaData()) { 377 lastInteresting = pos; 378 historyEnd = rec.time; 379 } 380 } 381 382 endWalltime = lastWallTime + historyEnd - lastRealtime; 383 384 int i = 0; 385 final int N = lastInteresting; 386 387 for (int j = 0; j < parsers.length; j++) { 388 parsers[j].onParsingStarted(startWalltime, endWalltime); 389 } 390 391 if (endWalltime > startWalltime) { 392 final BatteryStatsHistoryIterator iterator2 = 393 mBatteryUsageStats.iterateBatteryStatsHistory(); 394 while (iterator2.next(rec) && i < N) { 395 if (rec.isDeltaData()) { 396 curWalltime += rec.time - lastRealtime; 397 lastRealtime = rec.time; 398 long x = (curWalltime - startWalltime); 399 if (x < 0) { 400 x = 0; 401 } 402 for (int j = 0; j < parsers.length; j++) { 403 parsers[j].onDataPoint(x, rec); 404 } 405 } else { 406 long lastWalltime = curWalltime; 407 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 408 || rec.cmd == HistoryItem.CMD_RESET) { 409 if (rec.currentTime >= startWalltime) { 410 curWalltime = rec.currentTime; 411 } else { 412 curWalltime = startWalltime + (rec.time - historyStart); 413 } 414 lastRealtime = rec.time; 415 } 416 417 if (rec.cmd != HistoryItem.CMD_OVERFLOW 418 && (rec.cmd != HistoryItem.CMD_CURRENT_TIME 419 || Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) { 420 for (int j = 0; j < parsers.length; j++) { 421 parsers[j].onDataGap(); 422 } 423 } 424 } 425 i++; 426 } 427 } 428 429 for (int j = 0; j < parsers.length; j++) { 430 parsers[j].onParsingDone(); 431 } 432 } 433 } 434