• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.dialer.persistentlog;
18 
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.preference.PreferenceManager;
22 import android.support.annotation.AnyThread;
23 import android.support.annotation.MainThread;
24 import android.support.annotation.NonNull;
25 import android.support.annotation.Nullable;
26 import android.support.annotation.WorkerThread;
27 import android.support.v4.os.UserManagerCompat;
28 import com.android.dialer.common.LogUtil;
29 import java.io.ByteArrayInputStream;
30 import java.io.DataInputStream;
31 import java.io.DataOutputStream;
32 import java.io.EOFException;
33 import java.io.File;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.RandomAccessFile;
37 import java.nio.ByteBuffer;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.List;
41 
42 /**
43  * Handles serialization of byte arrays and read/write them to multiple rotating files. If a logText
44  * file exceeds {@code fileSizeLimit} after a write, a new file will be used. if the total number of
45  * files exceeds {@code fileCountLimit} the oldest ones will be deleted. The logs are stored in the
46  * cache but the file index is stored in the data (clearing data will also clear the cache). The
47  * logs will be stored under /cache_dir/persistent_log/{@code subfolder}, so multiple independent
48  * logs can be created.
49  *
50  * <p>This class is NOT thread safe. All methods expect the constructor must be called on the same
51  * worker thread.
52  */
53 final class PersistentLogFileHandler {
54 
55   private static final String LOG_DIRECTORY = "persistent_log";
56   private static final String NEXT_FILE_INDEX_PREFIX = "persistent_long_next_file_index_";
57 
58   private static final byte[] ENTRY_PREFIX = {'P'};
59   private static final byte[] ENTRY_POSTFIX = {'L'};
60 
61   private static class LogCorruptionException extends Exception {
62 
LogCorruptionException(String message)63     public LogCorruptionException(String message) {
64       super(message);
65     }
66   }
67 
68   private File logDirectory;
69   private final String subfolder;
70   private final int fileSizeLimit;
71   private final int fileCountLimit;
72 
73   private SharedPreferences sharedPreferences;
74 
75   private File outputFile;
76   private Context context;
77 
78   @MainThread
PersistentLogFileHandler(String subfolder, int fileSizeLimit, int fileCountLimit)79   PersistentLogFileHandler(String subfolder, int fileSizeLimit, int fileCountLimit) {
80     this.subfolder = subfolder;
81     this.fileSizeLimit = fileSizeLimit;
82     this.fileCountLimit = fileCountLimit;
83   }
84 
85   /** Must be called right after the logger thread is created. */
86   @WorkerThread
initialize(Context context)87   void initialize(Context context) {
88     this.context = context;
89     logDirectory = new File(new File(context.getCacheDir(), LOG_DIRECTORY), subfolder);
90     initializeSharedPreference(context);
91   }
92 
93   @WorkerThread
initializeSharedPreference(Context context)94   private boolean initializeSharedPreference(Context context) {
95     if (sharedPreferences == null && UserManagerCompat.isUserUnlocked(context)) {
96       sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
97       return true;
98     }
99     return sharedPreferences != null;
100   }
101 
102   /**
103    * Write the list of byte arrays to the current log file, prefixing each entry with its' length. A
104    * new file will only be selected when the batch is completed, so the resulting file might be
105    * larger then {@code fileSizeLimit}
106    */
107   @WorkerThread
writeLogs(List<byte[]> logs)108   void writeLogs(List<byte[]> logs) throws IOException {
109     if (outputFile == null) {
110       selectNextFileToWrite();
111     }
112     outputFile.createNewFile();
113     try (DataOutputStream outputStream =
114         new DataOutputStream(new FileOutputStream(outputFile, true))) {
115       for (byte[] log : logs) {
116         outputStream.write(ENTRY_PREFIX);
117         outputStream.writeInt(log.length);
118         outputStream.write(log);
119         outputStream.write(ENTRY_POSTFIX);
120       }
121       outputStream.close();
122       if (outputFile.length() > fileSizeLimit) {
123         selectNextFileToWrite();
124       }
125     }
126   }
127 
writeRawLogsForTest(byte[] data)128   void writeRawLogsForTest(byte[] data) throws IOException {
129     if (outputFile == null) {
130       selectNextFileToWrite();
131     }
132     outputFile.createNewFile();
133     try (DataOutputStream outputStream =
134         new DataOutputStream(new FileOutputStream(outputFile, true))) {
135       outputStream.write(data);
136       outputStream.close();
137       if (outputFile.length() > fileSizeLimit) {
138         selectNextFileToWrite();
139       }
140     }
141   }
142 
143   /** Concatenate all log files in chronicle order and return a byte array. */
144   @WorkerThread
145   @NonNull
readBlob()146   private byte[] readBlob() throws IOException {
147     File[] files = getLogFiles();
148 
149     ByteBuffer byteBuffer = ByteBuffer.allocate(getTotalSize(files));
150     for (File file : files) {
151       byteBuffer.put(readAllBytes(file));
152     }
153     return byteBuffer.array();
154   }
155 
getTotalSize(File[] files)156   private static int getTotalSize(File[] files) {
157     int sum = 0;
158     for (File file : files) {
159       sum += (int) file.length();
160     }
161     return sum;
162   }
163 
164   /** Parses the content of all files back to individual byte arrays. */
165   @WorkerThread
166   @NonNull
getLogs()167   List<byte[]> getLogs() throws IOException {
168     byte[] blob = readBlob();
169     List<byte[]> logs = new ArrayList<>();
170     try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(blob))) {
171       byte[] log = readLog(input);
172       while (log != null) {
173         logs.add(log);
174         log = readLog(input);
175       }
176     } catch (LogCorruptionException e) {
177       LogUtil.e("PersistentLogFileHandler.getLogs", "logs corrupted, deleting", e);
178       deleteLogs();
179       return new ArrayList<>();
180     }
181     return logs;
182   }
183 
deleteLogs()184   private void deleteLogs() throws IOException {
185     for (File file : getLogFiles()) {
186       file.delete();
187     }
188     selectNextFileToWrite();
189   }
190 
191   @WorkerThread
selectNextFileToWrite()192   private void selectNextFileToWrite() throws IOException {
193     File[] files = getLogFiles();
194 
195     if (files.length == 0 || files[files.length - 1].length() > fileSizeLimit) {
196       if (files.length >= fileCountLimit) {
197         for (int i = 0; i <= files.length - fileCountLimit; i++) {
198           files[i].delete();
199         }
200       }
201       outputFile = new File(logDirectory, String.valueOf(getAndIncrementNextFileIndex()));
202     } else {
203       outputFile = files[files.length - 1];
204     }
205   }
206 
207   @NonNull
208   @WorkerThread
getLogFiles()209   private File[] getLogFiles() {
210     logDirectory.mkdirs();
211     File[] files = logDirectory.listFiles();
212     if (files == null) {
213       files = new File[0];
214     }
215     Arrays.sort(
216         files,
217         (File lhs, File rhs) ->
218             Long.compare(Long.valueOf(lhs.getName()), Long.valueOf(rhs.getName())));
219     return files;
220   }
221 
222   @Nullable
223   @WorkerThread
readLog(DataInputStream inputStream)224   private byte[] readLog(DataInputStream inputStream) throws IOException, LogCorruptionException {
225     try {
226       byte[] prefix = new byte[ENTRY_PREFIX.length];
227       if (inputStream.read(prefix) == -1) {
228         // EOF
229         return null;
230       }
231       if (!Arrays.equals(prefix, ENTRY_PREFIX)) {
232         throw new LogCorruptionException("entry prefix mismatch");
233       }
234       int dataLength = inputStream.readInt();
235       if (dataLength > fileSizeLimit) {
236         throw new LogCorruptionException("data length over max size");
237       }
238       byte[] data = new byte[dataLength];
239       inputStream.read(data);
240 
241       byte[] postfix = new byte[ENTRY_POSTFIX.length];
242       inputStream.read(postfix);
243       if (!Arrays.equals(postfix, ENTRY_POSTFIX)) {
244         throw new LogCorruptionException("entry postfix mismatch");
245       }
246       return data;
247     } catch (EOFException e) {
248       return null;
249     }
250   }
251 
252   @NonNull
253   @WorkerThread
readAllBytes(File file)254   private static byte[] readAllBytes(File file) throws IOException {
255     byte[] result = new byte[(int) file.length()];
256     try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
257       randomAccessFile.readFully(result);
258     }
259     return result;
260   }
261 
262   @WorkerThread
getAndIncrementNextFileIndex()263   private int getAndIncrementNextFileIndex() throws IOException {
264     if (!initializeSharedPreference(context)) {
265       throw new IOException("Shared preference is not available");
266     }
267 
268     int index = sharedPreferences.getInt(getNextFileKey(), 0);
269     sharedPreferences.edit().putInt(getNextFileKey(), index + 1).commit();
270     return index;
271   }
272 
273   @AnyThread
getNextFileKey()274   private String getNextFileKey() {
275     return NEXT_FILE_INDEX_PREFIX + subfolder;
276   }
277 }
278