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