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 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 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 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 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 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 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 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 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 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 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 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 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 221 private String getNextFileKey() { 222 return NEXT_FILE_INDEX_PREFIX + subfolder; 223 } 224 } 225