1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.os; 18 19 import android.os.BatteryStats; 20 import android.os.Parcel; 21 import android.os.StatFs; 22 import android.os.SystemClock; 23 import android.util.ArraySet; 24 import android.util.Slog; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.internal.util.ParseUtils; 28 29 import java.io.File; 30 import java.io.FilenameFilter; 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.List; 34 import java.util.Set; 35 36 /** 37 * BatteryStatsHistory encapsulates battery history files. 38 * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into 39 * {@link #mActiveFile}. 40 * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER}, 41 * current mActiveFile is closed and a new mActiveFile is open. 42 * History files are under directory /data/system/battery-history/. 43 * History files have name battery-history-<num>.bin. The file number <num> starts from zero and 44 * grows sequentially. 45 * The mActiveFile is always the highest numbered history file. 46 * The lowest number file is always the oldest file. 47 * The highest number file is always the newest file. 48 * The file number grows sequentially and we never skip number. 49 * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES}, 50 * the lowest numbered file is deleted and a new file is open. 51 * 52 * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by 53 * locks on BatteryStatsImpl object. 54 */ 55 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 56 public class BatteryStatsHistory { 57 private static final boolean DEBUG = false; 58 private static final String TAG = "BatteryStatsHistory"; 59 public static final String HISTORY_DIR = "battery-history"; 60 public static final String FILE_SUFFIX = ".bin"; 61 private static final int MIN_FREE_SPACE = 100 * 1024 * 1024; 62 63 private final BatteryStatsImpl mStats; 64 private final Parcel mHistoryBuffer; 65 private final File mHistoryDir; 66 /** 67 * The active history file that the history buffer is backed up into. 68 */ 69 private AtomicFile mActiveFile; 70 /** 71 * A list of history files with incremental indexes. 72 */ 73 private final List<Integer> mFileNumbers = new ArrayList<>(); 74 75 /** 76 * A list of small history parcels, used when BatteryStatsImpl object is created from 77 * deserialization of a parcel, such as Settings app or checkin file. 78 */ 79 private List<Parcel> mHistoryParcels = null; 80 81 /** 82 * When iterating history files, the current file index. 83 */ 84 private int mCurrentFileIndex; 85 /** 86 * When iterating history files, the current file parcel. 87 */ 88 private Parcel mCurrentParcel; 89 /** 90 * When iterating history file, the current parcel's Parcel.dataSize(). 91 */ 92 private int mCurrentParcelEnd; 93 /** 94 * When iterating history files, the current record count. 95 */ 96 private int mRecordCount = 0; 97 /** 98 * Used when BatteryStatsImpl object is created from deserialization of a parcel, 99 * such as Settings app or checkin file, to iterate over history parcels. 100 */ 101 private int mParcelIndex = 0; 102 103 /** 104 * Constructor 105 * @param stats BatteryStatsImpl object. 106 * @param systemDir typically /data/system 107 * @param historyBuffer The in-memory history buffer. 108 */ BatteryStatsHistory(BatteryStatsImpl stats, File systemDir, Parcel historyBuffer)109 public BatteryStatsHistory(BatteryStatsImpl stats, File systemDir, Parcel historyBuffer) { 110 mStats = stats; 111 mHistoryBuffer = historyBuffer; 112 mHistoryDir = new File(systemDir, HISTORY_DIR); 113 mHistoryDir.mkdirs(); 114 if (!mHistoryDir.exists()) { 115 Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath()); 116 } 117 118 final Set<Integer> dedup = new ArraySet<>(); 119 // scan directory, fill mFileNumbers and mActiveFile. 120 mHistoryDir.listFiles(new FilenameFilter() { 121 @Override 122 public boolean accept(File dir, String name) { 123 final int b = name.lastIndexOf(FILE_SUFFIX); 124 if (b <= 0) { 125 return false; 126 } 127 final Integer c = 128 ParseUtils.parseInt(name.substring(0, b), -1); 129 if (c != -1) { 130 dedup.add(c); 131 return true; 132 } else { 133 return false; 134 } 135 } 136 }); 137 if (!dedup.isEmpty()) { 138 mFileNumbers.addAll(dedup); 139 Collections.sort(mFileNumbers); 140 setActiveFile(mFileNumbers.get(mFileNumbers.size() - 1)); 141 } else { 142 // No file found, default to have file 0. 143 mFileNumbers.add(0); 144 setActiveFile(0); 145 } 146 } 147 148 /** 149 * Used when BatteryStatsImpl object is created from deserialization of a parcel, 150 * such as Settings app or checkin file. 151 * @param stats BatteryStatsImpl object. 152 * @param historyBuffer the history buffer inside BatteryStatsImpl 153 */ BatteryStatsHistory(BatteryStatsImpl stats, Parcel historyBuffer)154 public BatteryStatsHistory(BatteryStatsImpl stats, Parcel historyBuffer) { 155 mStats = stats; 156 mHistoryDir = null; 157 mHistoryBuffer = historyBuffer; 158 } 159 /** 160 * Set the active file that mHistoryBuffer is backed up into. 161 * 162 * @param fileNumber the history file that mHistoryBuffer is backed up into. 163 */ setActiveFile(int fileNumber)164 private void setActiveFile(int fileNumber) { 165 mActiveFile = getFile(fileNumber); 166 if (DEBUG) { 167 Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath()); 168 } 169 } 170 171 /** 172 * Create history AtomicFile from file number. 173 * @param num file number. 174 * @return AtomicFile object. 175 */ getFile(int num)176 private AtomicFile getFile(int num) { 177 return new AtomicFile( 178 new File(mHistoryDir, num + FILE_SUFFIX)); 179 } 180 181 /** 182 * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER}, 183 * create next history file. 184 */ startNextFile()185 public void startNextFile() { 186 if (mFileNumbers.isEmpty()) { 187 Slog.wtf(TAG, "mFileNumbers should never be empty"); 188 return; 189 } 190 // The last number in mFileNumbers is the highest number. The next file number is highest 191 // number plus one. 192 final int next = mFileNumbers.get(mFileNumbers.size() - 1) + 1; 193 mFileNumbers.add(next); 194 setActiveFile(next); 195 196 // if free disk space is less than 100MB, delete oldest history file. 197 if (!hasFreeDiskSpace()) { 198 int oldest = mFileNumbers.remove(0); 199 getFile(oldest).delete(); 200 } 201 202 // if there are more history files than allowed, delete oldest history files. 203 // MAX_HISTORY_FILES can be updated by GService config at run time. 204 while (mFileNumbers.size() > mStats.mConstants.MAX_HISTORY_FILES) { 205 int oldest = mFileNumbers.get(0); 206 getFile(oldest).delete(); 207 mFileNumbers.remove(0); 208 } 209 } 210 211 /** 212 * Delete all existing history files. Active history file start from number 0 again. 213 */ resetAllFiles()214 public void resetAllFiles() { 215 for (Integer i : mFileNumbers) { 216 getFile(i).delete(); 217 } 218 mFileNumbers.clear(); 219 mFileNumbers.add(0); 220 setActiveFile(0); 221 } 222 223 /** 224 * Start iterating history files and history buffer. 225 * @return always return true. 226 */ startIteratingHistory()227 public boolean startIteratingHistory() { 228 mRecordCount = 0; 229 mCurrentFileIndex = 0; 230 mCurrentParcel = null; 231 mCurrentParcelEnd = 0; 232 mParcelIndex = 0; 233 return true; 234 } 235 236 /** 237 * Finish iterating history files and history buffer. 238 */ finishIteratingHistory()239 public void finishIteratingHistory() { 240 // setDataPosition so mHistoryBuffer Parcel can be written. 241 mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); 242 if (DEBUG) { 243 Slog.d(TAG, "Battery history records iterated: " + mRecordCount); 244 } 245 } 246 247 /** 248 * When iterating history files and history buffer, always start from the lowest numbered 249 * history file, when reached the mActiveFile (highest numbered history file), do not read from 250 * mActiveFile, read from history buffer instead because the buffer has more updated data. 251 * @param out a history item. 252 * @return The parcel that has next record. null if finished all history files and history 253 * buffer 254 */ getNextParcel(BatteryStats.HistoryItem out)255 public Parcel getNextParcel(BatteryStats.HistoryItem out) { 256 if (mRecordCount == 0) { 257 // reset out if it is the first record. 258 out.clear(); 259 } 260 ++mRecordCount; 261 262 // First iterate through all records in current parcel. 263 if (mCurrentParcel != null) 264 { 265 if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) { 266 // There are more records in current parcel. 267 return mCurrentParcel; 268 } else if (mHistoryBuffer == mCurrentParcel) { 269 // finished iterate through all history files and history buffer. 270 return null; 271 } else if (mHistoryParcels == null 272 || !mHistoryParcels.contains(mCurrentParcel)) { 273 // current parcel is from history file. 274 mCurrentParcel.recycle(); 275 } 276 } 277 278 // Try next available history file. 279 // skip the last file because its data is in history buffer. 280 while (mCurrentFileIndex < mFileNumbers.size() - 1) { 281 mCurrentParcel = null; 282 mCurrentParcelEnd = 0; 283 final Parcel p = Parcel.obtain(); 284 AtomicFile file = getFile(mFileNumbers.get(mCurrentFileIndex++)); 285 if (readFileToParcel(p, file)) { 286 int bufSize = p.readInt(); 287 int curPos = p.dataPosition(); 288 mCurrentParcelEnd = curPos + bufSize; 289 mCurrentParcel = p; 290 if (curPos < mCurrentParcelEnd) { 291 return mCurrentParcel; 292 } 293 } else { 294 p.recycle(); 295 } 296 } 297 298 // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization 299 // of a parcel, such as Settings app or checkin file. 300 if (mHistoryParcels != null) { 301 while (mParcelIndex < mHistoryParcels.size()) { 302 final Parcel p = mHistoryParcels.get(mParcelIndex++); 303 if (!skipHead(p)) { 304 continue; 305 } 306 final int bufSize = p.readInt(); 307 final int curPos = p.dataPosition(); 308 mCurrentParcelEnd = curPos + bufSize; 309 mCurrentParcel = p; 310 if (curPos < mCurrentParcelEnd) { 311 return mCurrentParcel; 312 } 313 } 314 } 315 316 // finished iterator through history files (except the last one), now history buffer. 317 if (mHistoryBuffer.dataSize() <= 0) { 318 // buffer is empty. 319 return null; 320 } 321 mHistoryBuffer.setDataPosition(0); 322 mCurrentParcel = mHistoryBuffer; 323 mCurrentParcelEnd = mCurrentParcel.dataSize(); 324 return mCurrentParcel; 325 } 326 327 /** 328 * Read history file into a parcel. 329 * @param out the Parcel read into. 330 * @param file the File to read from. 331 * @return true if success, false otherwise. 332 */ readFileToParcel(Parcel out, AtomicFile file)333 public boolean readFileToParcel(Parcel out, AtomicFile file) { 334 byte[] raw = null; 335 try { 336 final long start = SystemClock.uptimeMillis(); 337 raw = file.readFully(); 338 if (DEBUG) { 339 Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() 340 + " duration ms:" + (SystemClock.uptimeMillis() - start)); 341 } 342 } catch(Exception e) { 343 Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e); 344 return false; 345 } 346 out.unmarshall(raw, 0, raw.length); 347 out.setDataPosition(0); 348 return skipHead(out); 349 } 350 351 /** 352 * Skip the header part of history parcel. 353 * @param p history parcel to skip head. 354 * @return true if version match, false if not. 355 */ skipHead(Parcel p)356 private boolean skipHead(Parcel p) { 357 p.setDataPosition(0); 358 final int version = p.readInt(); 359 if (version != mStats.VERSION) { 360 return false; 361 } 362 // skip historyBaseTime field. 363 p.readLong(); 364 return true; 365 } 366 367 /** 368 * Read all history files and serialize into a big Parcel. This is to send history files to 369 * Settings app since Settings app can not access /data/system directory. 370 * Checkin file also call this method. 371 * @param out the output parcel 372 */ writeToParcel(Parcel out)373 public void writeToParcel(Parcel out) { 374 final long start = SystemClock.uptimeMillis(); 375 out.writeInt(mFileNumbers.size() - 1); 376 for(int i = 0; i < mFileNumbers.size() - 1; i++) { 377 AtomicFile file = getFile(mFileNumbers.get(i)); 378 byte[] raw = new byte[0]; 379 try { 380 raw = file.readFully(); 381 } catch(Exception e) { 382 Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e); 383 } 384 out.writeByteArray(raw); 385 } 386 if (DEBUG) { 387 Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start)); 388 } 389 } 390 391 /** 392 * This is for Settings app, when Settings app receives big history parcel, it call 393 * this method to parse it into list of parcels. 394 * Checkin file also call this method. 395 * @param in the input parcel. 396 */ readFromParcel(Parcel in)397 public void readFromParcel(Parcel in) { 398 final long start = SystemClock.uptimeMillis(); 399 mHistoryParcels = new ArrayList<>(); 400 final int count = in.readInt(); 401 for(int i = 0; i < count; i++) { 402 byte[] temp = in.createByteArray(); 403 if (temp.length == 0) { 404 continue; 405 } 406 Parcel p = Parcel.obtain(); 407 p.unmarshall(temp, 0, temp.length); 408 p.setDataPosition(0); 409 mHistoryParcels.add(p); 410 } 411 if (DEBUG) { 412 Slog.d(TAG, "readFromParcel duration ms:" + (SystemClock.uptimeMillis() - start)); 413 } 414 } 415 416 /** 417 * @return true if there is more than 100MB free disk space left. 418 */ hasFreeDiskSpace()419 private boolean hasFreeDiskSpace() { 420 final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath()); 421 return stats.getAvailableBytes() > MIN_FREE_SPACE; 422 } 423 getFilesNumbers()424 public List<Integer> getFilesNumbers() { 425 return mFileNumbers; 426 } 427 getActiveFile()428 public AtomicFile getActiveFile() { 429 return mActiveFile; 430 } 431 432 /** 433 * @return the total size of all history files and history buffer. 434 */ getHistoryUsedSize()435 public int getHistoryUsedSize() { 436 int ret = 0; 437 for(int i = 0; i < mFileNumbers.size() - 1; i++) { 438 ret += getFile(mFileNumbers.get(i)).getBaseFile().length(); 439 } 440 ret += mHistoryBuffer.dataSize(); 441 if (mHistoryParcels != null) { 442 for(int i = 0; i < mHistoryParcels.size(); i++) { 443 ret += mHistoryParcels.get(i).dataSize(); 444 } 445 } 446 return ret; 447 } 448 } 449