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