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 package com.android.wallpaper.util; 17 18 import static java.nio.charset.StandardCharsets.UTF_8; 19 20 import android.content.Context; 21 import android.os.Build; 22 import android.os.Handler; 23 import android.os.HandlerThread; 24 import android.os.Process; 25 import android.util.Log; 26 27 import com.android.wallpaper.compat.BuildCompat; 28 29 import java.io.BufferedReader; 30 import java.io.Closeable; 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.io.InputStreamReader; 36 import java.io.OutputStream; 37 import java.text.ParseException; 38 import java.text.SimpleDateFormat; 39 import java.util.Calendar; 40 import java.util.Date; 41 import java.util.Locale; 42 import java.util.concurrent.TimeUnit; 43 44 import androidx.annotation.Nullable; 45 import androidx.annotation.VisibleForTesting; 46 47 /** 48 * Logs messages to logcat and for debuggable build types ("eng" or "userdebug") also mirrors logs 49 * to a disk-based log buffer. 50 */ 51 public class DiskBasedLogger { 52 53 static final String LOGS_FILE_PATH = "logs.txt"; 54 static final SimpleDateFormat DATE_FORMAT = 55 new SimpleDateFormat("EEE MMM dd HH:mm:ss.SSS z yyyy", Locale.US); 56 57 private static final String TEMP_LOGS_FILE_PATH = "temp_logs.txt"; 58 private static final String TAG = "DiskBasedLogger"; 59 60 /** 61 * POJO used to lock thread creation and file read/write operations. 62 */ 63 private static final Object S_LOCK = new Object(); 64 65 private static final long THREAD_TIMEOUT_MILLIS = 66 TimeUnit.MILLISECONDS.convert(2, TimeUnit.MINUTES); 67 private static Handler sHandler; 68 private static HandlerThread sLoggerThread; 69 private static final Runnable THREAD_CLEANUP_RUNNABLE = new Runnable() { 70 @Override 71 public void run() { 72 if (sLoggerThread != null && sLoggerThread.isAlive()) { 73 74 // HandlerThread#quitSafely was added in JB-MR2, so prefer to use that instead of #quit. 75 boolean isQuitSuccessful = BuildCompat.isAtLeastJBMR2() 76 ? sLoggerThread.quitSafely() 77 : sLoggerThread.quit(); 78 79 if (!isQuitSuccessful) { 80 Log.e(TAG, "Unable to quit disk-based logger HandlerThread"); 81 } 82 83 sLoggerThread = null; 84 sHandler = null; 85 } 86 } 87 }; 88 89 /** 90 * Initializes and returns a new dedicated HandlerThread for reading and writing to the disk-based 91 * logs file. 92 */ initializeLoggerThread()93 private static void initializeLoggerThread() { 94 sLoggerThread = new HandlerThread("DiskBasedLoggerThread", Process.THREAD_PRIORITY_BACKGROUND); 95 sLoggerThread.start(); 96 } 97 98 /** 99 * Returns a Handler that can post messages to the dedicated HandlerThread for reading and writing 100 * to the logs file on disk. Lazy-loads the HandlerThread if it doesn't already exist and delays 101 * its death by a timeout if the thread already exists. 102 */ getLoggerThreadHandler()103 private static Handler getLoggerThreadHandler() { 104 synchronized (S_LOCK) { 105 if (sLoggerThread == null) { 106 initializeLoggerThread(); 107 108 // Create a new Handler tied to the new HandlerThread's Looper for processing disk I/O off 109 // the main thread. Starts with a default timeout to quit and remove references to the 110 // thread after a period of inactivity. 111 sHandler = new Handler(sLoggerThread.getLooper()); 112 } else { 113 sHandler.removeCallbacks(THREAD_CLEANUP_RUNNABLE); 114 } 115 116 // Delay the logger thread's eventual death. 117 sHandler.postDelayed(THREAD_CLEANUP_RUNNABLE, THREAD_TIMEOUT_MILLIS); 118 119 return sHandler; 120 } 121 } 122 123 /** 124 * Logs an "error" level log to logcat based on the provided tag and message and also duplicates 125 * the log to a file-based log buffer if running on a "userdebug" or "eng" build. 126 */ e(String tag, String msg, Context context)127 public static void e(String tag, String msg, Context context) { 128 // Pass log tag and message through to logcat regardless of build type. 129 Log.e(tag, msg); 130 131 // Only mirror logs to disk-based log buffer if the build is debuggable. 132 if (!Build.TYPE.equals("eng") && !Build.TYPE.equals("userdebug")) { 133 return; 134 } 135 136 Handler handler = getLoggerThreadHandler(); 137 if (handler == null) { 138 Log.e(TAG, "Something went wrong creating the logger thread handler, quitting this logging " 139 + "operation"); 140 return; 141 } 142 143 handler.post(() -> { 144 File logs = new File(context.getFilesDir(), LOGS_FILE_PATH); 145 146 // Construct a log message that we can parse later in order to clean up old logs. 147 String datetime = DATE_FORMAT.format(Calendar.getInstance().getTime()); 148 String log = datetime + "/E " + tag + ": " + msg + "\n"; 149 150 synchronized (S_LOCK) { 151 FileOutputStream outputStream; 152 153 try { 154 outputStream = context.openFileOutput(logs.getName(), Context.MODE_APPEND); 155 outputStream.write(log.getBytes(UTF_8)); 156 outputStream.close(); 157 } catch (IOException e) { 158 Log.e(TAG, "Unable to close output stream for disk-based log buffer", e); 159 } 160 } 161 }); 162 } 163 164 /** 165 * Deletes logs in the disk-based log buffer older than 7 days. 166 */ clearOldLogs(Context context)167 public static void clearOldLogs(Context context) { 168 if (!Build.TYPE.equals("eng") && !Build.TYPE.equals("userdebug")) { 169 return; 170 } 171 172 Handler handler = getLoggerThreadHandler(); 173 if (handler == null) { 174 Log.e(TAG, "Something went wrong creating the logger thread handler, quitting this logging " 175 + "operation"); 176 return; 177 } 178 179 handler.post(() -> { 180 // Check if the logs file exists first before trying to read from it. 181 File logsFile = new File(context.getFilesDir(), LOGS_FILE_PATH); 182 if (!logsFile.exists()) { 183 Log.w(TAG, "Disk-based log buffer doesn't exist, so there's nothing to clean up."); 184 return; 185 } 186 187 synchronized (S_LOCK) { 188 FileInputStream inputStream; 189 BufferedReader bufferedReader; 190 191 try { 192 inputStream = context.openFileInput(LOGS_FILE_PATH); 193 bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF_8)); 194 } catch (IOException e) { 195 Log.e(TAG, "IO exception opening a buffered reader for the existing logs file", e); 196 return; 197 } 198 199 Date sevenDaysAgo = getSevenDaysAgo(); 200 201 File tempLogsFile = new File(context.getFilesDir(), TEMP_LOGS_FILE_PATH); 202 FileOutputStream outputStream; 203 204 try { 205 outputStream = context.openFileOutput(TEMP_LOGS_FILE_PATH, Context.MODE_APPEND); 206 } catch (IOException e) { 207 Log.e(TAG, "Unable to close output stream for disk-based log buffer", e); 208 return; 209 } 210 211 copyLogsNewerThanDate(bufferedReader, outputStream, sevenDaysAgo); 212 213 // Close streams to prevent resource leaks. 214 closeStream(inputStream, "couldn't close input stream for log file"); 215 closeStream(outputStream, "couldn't close output stream for temp log file"); 216 217 // Rename temp log file (if it exists--which is only when the logs file has logs newer than 218 // 7 days to begin with) to the final logs file. 219 if (tempLogsFile.exists() && !tempLogsFile.renameTo(logsFile)) { 220 Log.e(TAG, "couldn't rename temp logs file to final logs file"); 221 } 222 } 223 }); 224 } 225 226 @Nullable 227 @VisibleForTesting getHandler()228 /* package */ static Handler getHandler() { 229 return sHandler; 230 } 231 232 /** 233 * Constructs and returns a {@link Date} object representing the time 7 days ago. 234 */ getSevenDaysAgo()235 private static Date getSevenDaysAgo() { 236 Calendar sevenDaysAgoCalendar = Calendar.getInstance(); 237 sevenDaysAgoCalendar.add(Calendar.DAY_OF_MONTH, -7); 238 return sevenDaysAgoCalendar.getTime(); 239 } 240 241 /** 242 * Tries to close the provided Closeable stream and logs the error message if the stream couldn't 243 * be closed. 244 */ closeStream(Closeable stream, String errorMessage)245 private static void closeStream(Closeable stream, String errorMessage) { 246 try { 247 stream.close(); 248 } catch (IOException e) { 249 Log.e(TAG, errorMessage); 250 } 251 } 252 253 /** 254 * Copies all log lines newer than the supplied date from the provided {@link BufferedReader} to 255 * the provided {@OutputStream}. 256 * <p> 257 * The caller of this method is responsible for closing the output stream and input stream 258 * underlying the BufferedReader when all operations have finished. 259 */ copyLogsNewerThanDate(BufferedReader reader, OutputStream outputStream, Date date)260 private static void copyLogsNewerThanDate(BufferedReader reader, OutputStream outputStream, 261 Date date) { 262 try { 263 String line = reader.readLine(); 264 while (line != null) { 265 // Get the date from the line string. 266 String datetime = line.split("/")[0]; 267 Date logDate; 268 try { 269 logDate = DATE_FORMAT.parse(datetime); 270 } catch (ParseException e) { 271 Log.e(TAG, "Error parsing date from previous logs", e); 272 return; 273 } 274 275 // Copy logs newer than the provided date into a temp log file. 276 if (logDate.after(date)) { 277 outputStream.write(line.getBytes(UTF_8)); 278 outputStream.write("\n".getBytes(UTF_8)); 279 } 280 281 line = reader.readLine(); 282 } 283 } catch (IOException e) { 284 Log.e(TAG, "IO exception while reading line from buffered reader", e); 285 } 286 } 287 } 288