1 /*
2  * Copyright (C) 2021 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.Nullable;
20 import android.content.Context;
21 import android.os.BatteryUsageStats;
22 import android.os.BatteryUsageStatsQuery;
23 import android.os.Handler;
24 import android.util.AtomicFile;
25 import android.util.LongArray;
26 import android.util.Slog;
27 import android.util.TypedXmlPullParser;
28 import android.util.TypedXmlSerializer;
29 import android.util.Xml;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 
33 import org.xmlpull.v1.XmlPullParserException;
34 
35 import java.io.File;
36 import java.io.FileInputStream;
37 import java.io.FileOutputStream;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.io.OutputStream;
41 import java.nio.channels.FileChannel;
42 import java.nio.channels.FileLock;
43 import java.nio.charset.StandardCharsets;
44 import java.nio.file.StandardOpenOption;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.Map;
48 import java.util.Properties;
49 import java.util.TreeMap;
50 
51 /**
52  * A storage mechanism for BatteryUsageStats snapshots.
53  */
54 public class BatteryUsageStatsStore {
55     private static final String TAG = "BatteryUsageStatsStore";
56 
57     private static final List<BatteryUsageStatsQuery> BATTERY_USAGE_STATS_QUERY = List.of(
58             new BatteryUsageStatsQuery.Builder()
59                     .setMaxStatsAgeMs(0)
60                     .includePowerModels()
61                     .build());
62     private static final String BATTERY_USAGE_STATS_DIR = "battery-usage-stats";
63     private static final String SNAPSHOT_FILE_EXTENSION = ".bus";
64     private static final String DIR_LOCK_FILENAME = ".lock";
65     private static final String CONFIG_FILENAME = "config";
66     private static final String BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY =
67             "BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP";
68     private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 100 * 1024;
69 
70     private final Context mContext;
71     private final BatteryStatsImpl mBatteryStats;
72     private boolean mSystemReady;
73     private final File mStoreDir;
74     private final File mLockFile;
75     private final AtomicFile mConfigFile;
76     private final long mMaxStorageBytes;
77     private final Handler mHandler;
78     private final BatteryUsageStatsProvider mBatteryUsageStatsProvider;
79 
BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir, Handler handler)80     public BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir,
81             Handler handler) {
82         this(context, stats, systemDir, handler, MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
83     }
84 
85     @VisibleForTesting
BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir, Handler handler, long maxStorageBytes)86     public BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir,
87             Handler handler, long maxStorageBytes) {
88         mContext = context;
89         mBatteryStats = batteryStats;
90         mStoreDir = new File(systemDir, BATTERY_USAGE_STATS_DIR);
91         mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME);
92         mConfigFile = new AtomicFile(new File(mStoreDir, CONFIG_FILENAME));
93         mHandler = handler;
94         mMaxStorageBytes = maxStorageBytes;
95         mBatteryStats.setBatteryResetListener(this::prepareForBatteryStatsReset);
96         mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(mContext, mBatteryStats);
97     }
98 
99     /**
100      * Notifies BatteryUsageStatsStore that the system server is ready.
101      */
onSystemReady()102     public void onSystemReady() {
103         mSystemReady = true;
104     }
105 
prepareForBatteryStatsReset(int resetReason)106     private void prepareForBatteryStatsReset(int resetReason) {
107         if (resetReason == BatteryStatsImpl.RESET_REASON_CORRUPT_FILE || !mSystemReady) {
108             return;
109         }
110 
111         final List<BatteryUsageStats> stats =
112                 mBatteryUsageStatsProvider.getBatteryUsageStats(BATTERY_USAGE_STATS_QUERY);
113         if (stats.isEmpty()) {
114             Slog.wtf(TAG, "No battery usage stats generated");
115             return;
116         }
117 
118         mHandler.post(() -> storeBatteryUsageStats(stats.get(0)));
119     }
120 
storeBatteryUsageStats(BatteryUsageStats stats)121     private void storeBatteryUsageStats(BatteryUsageStats stats) {
122         try (FileLock lock = lockSnapshotDirectory()) {
123             if (!mStoreDir.exists()) {
124                 if (!mStoreDir.mkdirs()) {
125                     Slog.e(TAG,
126                             "Could not create a directory for battery usage stats snapshots");
127                     return;
128                 }
129             }
130             File file = makeSnapshotFilename(stats.getStatsEndTimestamp());
131             try {
132                 writeXmlFileLocked(stats, file);
133             } catch (Exception e) {
134                 Slog.e(TAG, "Cannot save battery usage stats", e);
135             }
136 
137             removeOldSnapshotsLocked();
138         } catch (IOException e) {
139             Slog.e(TAG, "Cannot lock battery usage stats directory", e);
140         }
141     }
142 
143     /**
144      * Returns the timestamps of the stored BatteryUsageStats snapshots. The timestamp corresponds
145      * to the time the snapshot was taken {@link BatteryUsageStats#getStatsEndTimestamp()}.
146      */
listBatteryUsageStatsTimestamps()147     public long[] listBatteryUsageStatsTimestamps() {
148         LongArray timestamps = new LongArray(100);
149         try (FileLock lock = lockSnapshotDirectory()) {
150             for (File file : mStoreDir.listFiles()) {
151                 String fileName = file.getName();
152                 if (fileName.endsWith(SNAPSHOT_FILE_EXTENSION)) {
153                     try {
154                         String fileNameWithoutExtension = fileName.substring(0,
155                                 fileName.length() - SNAPSHOT_FILE_EXTENSION.length());
156                         timestamps.add(Long.parseLong(fileNameWithoutExtension));
157                     } catch (NumberFormatException e) {
158                         Slog.wtf(TAG, "Invalid format of BatteryUsageStats snapshot file name: "
159                                 + fileName);
160                     }
161                 }
162             }
163         } catch (IOException e) {
164             Slog.e(TAG, "Cannot lock battery usage stats directory", e);
165         }
166         return timestamps.toArray();
167     }
168 
169     /**
170      * Reads the specified snapshot of BatteryUsageStats.  Returns null if the snapshot
171      * does not exist.
172      */
173     @Nullable
loadBatteryUsageStats(long timestamp)174     public BatteryUsageStats loadBatteryUsageStats(long timestamp) {
175         try (FileLock lock = lockSnapshotDirectory()) {
176             File file = makeSnapshotFilename(timestamp);
177             try {
178                 return readXmlFileLocked(file);
179             } catch (Exception e) {
180                 Slog.e(TAG, "Cannot read battery usage stats", e);
181             }
182         } catch (IOException e) {
183             Slog.e(TAG, "Cannot lock battery usage stats directory", e);
184         }
185         return null;
186     }
187 
188     /**
189      * Saves the supplied timestamp of the BATTERY_USAGE_STATS_BEFORE_RESET statsd atom pull
190      * in persistent file.
191      */
setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(long timestamp)192     public void setLastBatteryUsageStatsBeforeResetAtomPullTimestamp(long timestamp) {
193         Properties props = new Properties();
194         try (FileLock lock = lockSnapshotDirectory()) {
195             try (InputStream in = mConfigFile.openRead()) {
196                 props.load(in);
197             } catch (IOException e) {
198                 Slog.e(TAG, "Cannot load config file " + mConfigFile, e);
199             }
200             props.put(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY,
201                     String.valueOf(timestamp));
202             FileOutputStream out = null;
203             try {
204                 out = mConfigFile.startWrite();
205                 props.store(out, "Statsd atom pull timestamps");
206                 mConfigFile.finishWrite(out);
207             } catch (IOException e) {
208                 mConfigFile.failWrite(out);
209                 Slog.e(TAG, "Cannot save config file " + mConfigFile, e);
210             }
211         } catch (IOException e) {
212             Slog.e(TAG, "Cannot lock battery usage stats directory", e);
213         }
214     }
215 
216     /**
217      * Retrieves the previously saved timestamp of the last BATTERY_USAGE_STATS_BEFORE_RESET
218      * statsd atom pull.
219      */
getLastBatteryUsageStatsBeforeResetAtomPullTimestamp()220     public long getLastBatteryUsageStatsBeforeResetAtomPullTimestamp() {
221         Properties props = new Properties();
222         try (FileLock lock = lockSnapshotDirectory()) {
223             try (InputStream in = mConfigFile.openRead()) {
224                 props.load(in);
225             } catch (IOException e) {
226                 Slog.e(TAG, "Cannot load config file " + mConfigFile, e);
227             }
228         } catch (IOException e) {
229             Slog.e(TAG, "Cannot lock battery usage stats directory", e);
230         }
231         return Long.parseLong(
232                 props.getProperty(BATTERY_USAGE_STATS_BEFORE_RESET_TIMESTAMP_PROPERTY, "0"));
233     }
234 
lockSnapshotDirectory()235     private FileLock lockSnapshotDirectory() throws IOException {
236         mLockFile.getParentFile().mkdirs();
237         mLockFile.createNewFile();
238         return FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock();
239     }
240 
241     /**
242      * Creates a file name by formatting the timestamp as 19-digit zero-padded number.
243      * This ensures that sorted directory list follows the chronological order.
244      */
makeSnapshotFilename(long statsEndTimestamp)245     private File makeSnapshotFilename(long statsEndTimestamp) {
246         return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", statsEndTimestamp)
247                 + SNAPSHOT_FILE_EXTENSION);
248     }
249 
writeXmlFileLocked(BatteryUsageStats stats, File file)250     private void writeXmlFileLocked(BatteryUsageStats stats, File file) throws IOException {
251         try (OutputStream out = new FileOutputStream(file)) {
252             TypedXmlSerializer serializer = Xml.newBinarySerializer();
253             serializer.setOutput(out, StandardCharsets.UTF_8.name());
254             serializer.startDocument(null, true);
255             stats.writeXml(serializer);
256             serializer.endDocument();
257         }
258     }
259 
readXmlFileLocked(File file)260     private BatteryUsageStats readXmlFileLocked(File file)
261             throws IOException, XmlPullParserException {
262         try (InputStream in = new FileInputStream(file)) {
263             TypedXmlPullParser parser = Xml.newBinaryPullParser();
264             parser.setInput(in, StandardCharsets.UTF_8.name());
265             return BatteryUsageStats.createFromXml(parser);
266         }
267     }
268 
removeOldSnapshotsLocked()269     private void removeOldSnapshotsLocked() {
270         // Read the directory list into a _sorted_ map.  The alphanumeric ordering
271         // corresponds to the historical order of snapshots because the file names
272         // are timestamps zero-padded to the same length.
273         long totalSize = 0;
274         TreeMap<File, Long> mFileSizes = new TreeMap<>();
275         for (File file : mStoreDir.listFiles()) {
276             final long fileSize = file.length();
277             totalSize += fileSize;
278             if (file.getName().endsWith(SNAPSHOT_FILE_EXTENSION)) {
279                 mFileSizes.put(file, fileSize);
280             }
281         }
282 
283         while (totalSize > mMaxStorageBytes) {
284             final Map.Entry<File, Long> entry = mFileSizes.firstEntry();
285             if (entry == null) {
286                 break;
287             }
288 
289             File file = entry.getKey();
290             if (!file.delete()) {
291                 Slog.e(TAG, "Cannot delete battery usage stats " + file);
292             }
293             totalSize -= entry.getValue();
294             mFileSizes.remove(file);
295         }
296     }
297 }
298