1 /*
2  * Copyright (C) 2020 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.server.powerstats;
18 
19 import android.content.Context;
20 import android.util.Slog;
21 
22 import com.android.internal.util.FileRotator;
23 
24 import java.io.ByteArrayOutputStream;
25 import java.io.File;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.nio.ByteBuffer;
30 import java.util.concurrent.locks.ReentrantLock;
31 
32 /**
33  * PowerStatsDataStorage implements the on-device storage cache for energy
34  * data.  This data must be persisted across boot cycles so we store it
35  * on-device.  Versioning of this data is handled by deleting any data that
36  * does not match the current version.  The cache is implemented as a circular
37  * buffer using the FileRotator class in android.util.  We maintain 48 hours
38  * worth of logs in 12 files (4 hours each).
39  */
40 public class PowerStatsDataStorage {
41     private static final String TAG = PowerStatsDataStorage.class.getSimpleName();
42 
43     private static final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60;
44     // Rotate files every 4 hours.
45     private static final long ROTATE_AGE_MILLIS = 4 * MILLISECONDS_PER_HOUR;
46     // Store 48 hours worth of data.
47     private static final long DELETE_AGE_MILLIS = 48 * MILLISECONDS_PER_HOUR;
48 
49     private final ReentrantLock mLock = new ReentrantLock();
50     private final File mDataStorageDir;
51     private final String mDataStorageFilename;
52     private final FileRotator mFileRotator;
53 
54     private static class DataElement {
55         private static final int LENGTH_FIELD_WIDTH = 4;
56         private static final int MAX_DATA_ELEMENT_SIZE = 1000;
57 
58         private byte[] mData;
59 
toByteArray()60         private byte[] toByteArray() throws IOException {
61             ByteArrayOutputStream data = new ByteArrayOutputStream();
62             data.write(ByteBuffer.allocate(LENGTH_FIELD_WIDTH).putInt(mData.length).array());
63             data.write(mData);
64             return data.toByteArray();
65         }
66 
getData()67         protected byte[] getData() {
68             return mData;
69         }
70 
DataElement(byte[] data)71         private DataElement(byte[] data) {
72             mData = data;
73         }
74 
DataElement(InputStream in)75         private DataElement(InputStream in) throws IOException {
76             byte[] lengthBytes = new byte[LENGTH_FIELD_WIDTH];
77             int bytesRead = in.read(lengthBytes);
78             mData = new byte[0];
79 
80             if (bytesRead == LENGTH_FIELD_WIDTH) {
81                 int length = ByteBuffer.wrap(lengthBytes).getInt();
82 
83                 if (0 < length && length < MAX_DATA_ELEMENT_SIZE) {
84                     mData = new byte[length];
85                     bytesRead = in.read(mData);
86 
87                     if (bytesRead != length) {
88                         throw new IOException("Invalid bytes read, expected: " + length
89                             + ", actual: " + bytesRead);
90                     }
91                 } else {
92                     throw new IOException("DataElement size is invalid: " + length);
93                 }
94             } else {
95                 throw new IOException("Did not read " + LENGTH_FIELD_WIDTH + " bytes (" + bytesRead
96                     + ")");
97             }
98         }
99     }
100 
101     /**
102      * Used by external classes to read DataElements from on-device storage.
103      * This callback is passed in to the read() function and is called for
104      * each DataElement read from on-device storage.
105      */
106     public interface DataElementReadCallback {
107         /**
108          * When performing a read of the on-device storage this callback
109          * must be passed in to the read function.  The function will be
110          * called for each DataElement read from on-device storage.
111          *
112          * @param data Byte array containing a DataElement payload.
113          */
onReadDataElement(byte[] data)114         void onReadDataElement(byte[] data);
115     }
116 
117     private static class DataReader implements FileRotator.Reader {
118         private DataElementReadCallback mCallback;
119 
DataReader(DataElementReadCallback callback)120         DataReader(DataElementReadCallback callback) {
121             mCallback = callback;
122         }
123 
124         @Override
read(InputStream in)125         public void read(InputStream in) throws IOException {
126             while (in.available() > 0) {
127                 DataElement dataElement = new DataElement(in);
128                 mCallback.onReadDataElement(dataElement.getData());
129             }
130         }
131     }
132 
133     private static class DataRewriter implements FileRotator.Rewriter {
134         byte[] mActiveFileData;
135         byte[] mNewData;
136 
DataRewriter(byte[] data)137         DataRewriter(byte[] data) {
138             mActiveFileData = new byte[0];
139             mNewData = data;
140         }
141 
142         @Override
reset()143         public void reset() {
144             // ignored
145         }
146 
147         @Override
read(InputStream in)148         public void read(InputStream in) throws IOException {
149             mActiveFileData = new byte[in.available()];
150             in.read(mActiveFileData);
151         }
152 
153         @Override
shouldWrite()154         public boolean shouldWrite() {
155             return true;
156         }
157 
158         @Override
write(OutputStream out)159         public void write(OutputStream out) throws IOException {
160             out.write(mActiveFileData);
161             out.write(mNewData);
162         }
163     }
164 
PowerStatsDataStorage(Context context, File dataStoragePath, String dataStorageFilename)165     public PowerStatsDataStorage(Context context, File dataStoragePath,
166             String dataStorageFilename) {
167         mDataStorageDir = dataStoragePath;
168         mDataStorageFilename = dataStorageFilename;
169 
170         if (!mDataStorageDir.exists() && !mDataStorageDir.mkdirs()) {
171             Slog.wtf(TAG, "mDataStorageDir does not exist: " + mDataStorageDir.getPath());
172             mFileRotator = null;
173         } else {
174             // Delete files written with an old version number.  The version is included in the
175             // filename, so any files that don't match the current version number can be deleted.
176             File[] files = mDataStorageDir.listFiles();
177             for (int i = 0; i < files.length; i++) {
178                 // Meter, model, and residency files are stored in the same directory.
179                 //
180                 // The format of filenames on disk is:
181                 //    log.powerstats.meter.version.timestamp
182                 //    log.powerstats.model.version.timestamp
183                 //    log.powerstats.residency.version.timestamp
184                 //
185                 // The format of dataStorageFilenames is:
186                 //    log.powerstats.meter.version
187                 //    log.powerstats.model.version
188                 //    log.powerstats.residency.version
189                 //
190                 // A PowerStatsDataStorage object is created for meter, model, and residency data.
191                 // Strip off the version and check that the current file we're checking starts with
192                 // the stem (log.powerstats.meter, log.powerstats.model, log.powerstats.residency).
193                 // If the stem matches and the version number is different, delete the old file.
194                 int versionDot = mDataStorageFilename.lastIndexOf('.');
195                 String beforeVersionDot = mDataStorageFilename.substring(0, versionDot);
196                 // Check that the stems match.
197                 if (files[i].getName().startsWith(beforeVersionDot)) {
198                     // Check that the version number matches.  If not, delete the old file.
199                     if (!files[i].getName().startsWith(mDataStorageFilename)) {
200                         files[i].delete();
201                     }
202                 }
203             }
204 
205             mFileRotator = new FileRotator(mDataStorageDir,
206                                            mDataStorageFilename,
207                                            ROTATE_AGE_MILLIS,
208                                            DELETE_AGE_MILLIS);
209         }
210     }
211 
212     /**
213      * Writes data stored in PowerStatsDataStorage to a file descriptor.
214      *
215      * @param data Byte array to write to on-device storage.  Byte array is
216      *             converted to a DataElement which prefixes the payload with
217      *             the data length.  The DataElement is then converted to a byte
218      *             array and written to on-device storage.
219      */
write(byte[] data)220     public void write(byte[] data) {
221         if (data != null && data.length > 0) {
222             mLock.lock();
223 
224             long currentTimeMillis = System.currentTimeMillis();
225             try {
226                 DataElement dataElement = new DataElement(data);
227                 mFileRotator.rewriteActive(new DataRewriter(dataElement.toByteArray()),
228                         currentTimeMillis);
229                 mFileRotator.maybeRotate(currentTimeMillis);
230             } catch (IOException e) {
231                 Slog.e(TAG, "Failed to write to on-device storage: " + e);
232             }
233 
234             mLock.unlock();
235         }
236     }
237 
238     /**
239      * Reads all DataElements stored in on-device storage.  For each
240      * DataElement retrieved from on-device storage, callback is called.
241      */
read(DataElementReadCallback callback)242     public void read(DataElementReadCallback callback) throws IOException {
243         mFileRotator.readMatching(new DataReader(callback), Long.MIN_VALUE, Long.MAX_VALUE);
244     }
245 
246     /**
247      * Deletes all stored log data.
248      */
deleteLogs()249     public void deleteLogs() {
250         File[] files = mDataStorageDir.listFiles();
251         for (int i = 0; i < files.length; i++) {
252             int versionDot = mDataStorageFilename.lastIndexOf('.');
253             String beforeVersionDot = mDataStorageFilename.substring(0, versionDot);
254             // Check that the stems before the version match.
255             if (files[i].getName().startsWith(beforeVersionDot)) {
256                 files[i].delete();
257             }
258         }
259     }
260 }
261