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