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