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