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