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