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.settingslib; 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.text.format.Formatter; 28 import android.util.SparseIntArray; 29 import com.android.internal.os.BatteryStatsHelper; 30 import com.android.settingslib.graph.UsageView; 31 32 public class BatteryInfo { 33 34 public String chargeLabelString; 35 public int batteryLevel; 36 public boolean discharging = true; 37 public long remainingTimeUs = 0; 38 public String batteryPercentString; 39 public String remainingLabel; 40 public String statusLabel; 41 private boolean mCharging; 42 private BatteryStats mStats; 43 private long timePeriod; 44 45 public interface Callback { 46 void onBatteryInfoLoaded(BatteryInfo info); 47 } 48 49 public void bindHistory(final UsageView view, BatteryDataParser... parsers) { 50 BatteryDataParser parser = new BatteryDataParser() { 51 SparseIntArray points = new SparseIntArray(); 52 53 @Override 54 public void onParsingStarted(long startTime, long endTime) { 55 timePeriod = endTime - startTime - remainingTimeUs / 1000; 56 view.clearPaths(); 57 view.configureGraph((int) (endTime - startTime), 100, remainingTimeUs != 0, 58 mCharging); 59 } 60 61 @Override 62 public void onDataPoint(long time, HistoryItem record) { 63 points.put((int) time, record.batteryLevel); 64 } 65 66 @Override 67 public void onDataGap() { 68 if (points.size() > 1) { 69 view.addPath(points); 70 } 71 points.clear(); 72 } 73 74 @Override 75 public void onParsingDone() { 76 if (points.size() > 1) { 77 view.addPath(points); 78 } 79 } 80 }; 81 BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1]; 82 for (int i = 0; i < parsers.length; i++) { 83 parserList[i] = parsers[i]; 84 } 85 parserList[parsers.length] = parser; 86 parse(mStats, remainingTimeUs, parserList); 87 final Context context = view.getContext(); 88 String timeString = context.getString(R.string.charge_length_format, 89 Formatter.formatShortElapsedTime(context, timePeriod)); 90 String remaining = ""; 91 if (remainingTimeUs != 0) { 92 remaining = context.getString(R.string.remaining_length_format, 93 Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000)); 94 } 95 view.setBottomLabels(new CharSequence[]{timeString, remaining}); 96 } 97 98 public static void getBatteryInfo(final Context context, final Callback callback) { 99 BatteryInfo.getBatteryInfo(context, callback, false /* shortString */); 100 } 101 102 public static void getBatteryInfo(final Context context, final Callback callback, 103 boolean shortString) { 104 new AsyncTask<Void, Void, BatteryStats>() { 105 @Override 106 protected BatteryStats doInBackground(Void... params) { 107 BatteryStatsHelper statsHelper = new BatteryStatsHelper(context, true); 108 statsHelper.create((Bundle) null); 109 return statsHelper.getStats(); 110 } 111 112 @Override 113 protected void onPostExecute(BatteryStats batteryStats) { 114 final long elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; 115 Intent batteryBroadcast = context.registerReceiver(null, 116 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 117 BatteryInfo batteryInfo = BatteryInfo.getBatteryInfo(context, batteryBroadcast, 118 batteryStats, elapsedRealtimeUs, shortString); 119 callback.onBatteryInfoLoaded(batteryInfo); 120 } 121 }.execute(); 122 } 123 124 public static BatteryInfo getBatteryInfo(Context context, Intent batteryBroadcast, 125 BatteryStats stats, long elapsedRealtimeUs) { 126 return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, elapsedRealtimeUs, 127 false /* shortString */); 128 } 129 130 public static BatteryInfo getBatteryInfo(Context context, Intent batteryBroadcast, 131 BatteryStats stats, long elapsedRealtimeUs, boolean shortString) { 132 BatteryInfo info = new BatteryInfo(); 133 info.mStats = stats; 134 info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast); 135 info.batteryPercentString = Utils.formatPercentage(info.batteryLevel); 136 info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; 137 final Resources resources = context.getResources(); 138 139 info.statusLabel = Utils.getBatteryStatus(resources, batteryBroadcast); 140 if (!info.mCharging) { 141 final long drainTime = stats.computeBatteryTimeRemaining(elapsedRealtimeUs); 142 if (drainTime > 0) { 143 info.remainingTimeUs = drainTime; 144 String timeString = Formatter.formatShortElapsedTime(context, 145 drainTime / 1000); 146 info.remainingLabel = resources.getString( 147 shortString ? R.string.power_remaining_duration_only_short 148 : R.string.power_remaining_duration_only, 149 timeString); 150 info.chargeLabelString = resources.getString( 151 shortString ? R.string.power_discharging_duration_short 152 : R.string.power_discharging_duration, 153 info.batteryPercentString, timeString); 154 } else { 155 info.remainingLabel = null; 156 info.chargeLabelString = info.batteryPercentString; 157 } 158 } else { 159 final long chargeTime = stats.computeChargeTimeRemaining(elapsedRealtimeUs); 160 final int status = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS, 161 BatteryManager.BATTERY_STATUS_UNKNOWN); 162 info.discharging = false; 163 if (chargeTime > 0 && status != BatteryManager.BATTERY_STATUS_FULL) { 164 info.remainingTimeUs = chargeTime; 165 String timeString = Formatter.formatShortElapsedTime(context, 166 chargeTime / 1000); 167 int resId = shortString ? R.string.power_charging_duration_short 168 : R.string.power_charging_duration; 169 info.remainingLabel = resources.getString( 170 R.string.power_remaining_charging_duration_only, timeString); 171 info.chargeLabelString = resources.getString( 172 resId, info.batteryPercentString, timeString); 173 } else { 174 final String chargeStatusLabel = resources.getString( 175 R.string.battery_info_status_charging_lower); 176 info.remainingLabel = null; 177 info.chargeLabelString = resources.getString( 178 R.string.power_charging, info.batteryPercentString, chargeStatusLabel); 179 } 180 } 181 return info; 182 } 183 184 public interface BatteryDataParser { 185 void onParsingStarted(long startTime, long endTime); 186 187 void onDataPoint(long time, HistoryItem record); 188 189 void onDataGap(); 190 191 void onParsingDone(); 192 } 193 194 private static void parse(BatteryStats stats, long remainingTimeUs, 195 BatteryDataParser... parsers) { 196 long startWalltime = 0; 197 long endDateWalltime = 0; 198 long endWalltime = 0; 199 long historyStart = 0; 200 long historyEnd = 0; 201 byte lastLevel = -1; 202 long curWalltime = startWalltime; 203 long lastWallTime = 0; 204 long lastRealtime = 0; 205 int lastInteresting = 0; 206 int pos = 0; 207 boolean first = true; 208 if (stats.startIteratingHistoryLocked()) { 209 final HistoryItem rec = new HistoryItem(); 210 while (stats.getNextHistoryLocked(rec)) { 211 pos++; 212 if (first) { 213 first = false; 214 historyStart = rec.time; 215 } 216 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 217 || rec.cmd == HistoryItem.CMD_RESET) { 218 // If there is a ridiculously large jump in time, then we won't be 219 // able to create a good chart with that data, so just ignore the 220 // times we got before and pretend like our data extends back from 221 // the time we have now. 222 // Also, if we are getting a time change and we are less than 5 minutes 223 // since the start of the history real time, then also use this new 224 // time to compute the base time, since whatever time we had before is 225 // pretty much just noise. 226 if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L)) 227 || rec.time < (historyStart + (5 * 60 * 1000L))) { 228 startWalltime = 0; 229 } 230 lastWallTime = rec.currentTime; 231 lastRealtime = rec.time; 232 if (startWalltime == 0) { 233 startWalltime = lastWallTime - (lastRealtime - historyStart); 234 } 235 } 236 if (rec.isDeltaData()) { 237 if (rec.batteryLevel != lastLevel || pos == 1) { 238 lastLevel = rec.batteryLevel; 239 } 240 lastInteresting = pos; 241 historyEnd = rec.time; 242 } 243 } 244 } 245 stats.finishIteratingHistoryLocked(); 246 endDateWalltime = lastWallTime + historyEnd - lastRealtime; 247 endWalltime = endDateWalltime + (remainingTimeUs / 1000); 248 249 int i = 0; 250 final int N = lastInteresting; 251 252 for (int j = 0; j < parsers.length; j++) { 253 parsers[j].onParsingStarted(startWalltime, endWalltime); 254 } 255 if (endDateWalltime > startWalltime && stats.startIteratingHistoryLocked()) { 256 final HistoryItem rec = new HistoryItem(); 257 while (stats.getNextHistoryLocked(rec) && i < N) { 258 if (rec.isDeltaData()) { 259 curWalltime += rec.time - lastRealtime; 260 lastRealtime = rec.time; 261 long x = (curWalltime - startWalltime); 262 if (x < 0) { 263 x = 0; 264 } 265 for (int j = 0; j < parsers.length; j++) { 266 parsers[j].onDataPoint(x, rec); 267 } 268 } else { 269 long lastWalltime = curWalltime; 270 if (rec.cmd == HistoryItem.CMD_CURRENT_TIME 271 || rec.cmd == HistoryItem.CMD_RESET) { 272 if (rec.currentTime >= startWalltime) { 273 curWalltime = rec.currentTime; 274 } else { 275 curWalltime = startWalltime + (rec.time - historyStart); 276 } 277 lastRealtime = rec.time; 278 } 279 280 if (rec.cmd != HistoryItem.CMD_OVERFLOW 281 && (rec.cmd != HistoryItem.CMD_CURRENT_TIME 282 || Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) { 283 for (int j = 0; j < parsers.length; j++) { 284 parsers[j].onDataGap(); 285 } 286 } 287 } 288 i++; 289 } 290 } 291 292 stats.finishIteratingHistoryLocked(); 293 294 for (int j = 0; j < parsers.length; j++) { 295 parsers[j].onParsingDone(); 296 } 297 } 298 } 299