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