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