blob: 5111376e367ca207ea27574423b0b7f35f040bae [file] [log] [blame]
Jon Miranda16ea1b12017-12-12 14:52:48 -08001/*
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 */
16package com.android.wallpaper.util;
17
Sunny Goyal8600a3f2018-08-15 12:48:01 -070018import static java.nio.charset.StandardCharsets.UTF_8;
19
Jon Miranda16ea1b12017-12-12 14:52:48 -080020import android.content.Context;
21import android.os.Build;
22import android.os.Handler;
23import android.os.HandlerThread;
24import android.os.Process;
Jon Miranda16ea1b12017-12-12 14:52:48 -080025import android.util.Log;
26
27import com.android.wallpaper.compat.BuildCompat;
28
29import java.io.BufferedReader;
30import java.io.Closeable;
31import java.io.File;
32import java.io.FileInputStream;
33import java.io.FileOutputStream;
34import java.io.IOException;
35import java.io.InputStreamReader;
36import java.io.OutputStream;
37import java.text.ParseException;
38import java.text.SimpleDateFormat;
39import java.util.Calendar;
40import java.util.Date;
41import java.util.Locale;
42import java.util.concurrent.TimeUnit;
43
Sunny Goyal8600a3f2018-08-15 12:48:01 -070044import androidx.annotation.Nullable;
45import androidx.annotation.VisibleForTesting;
Jon Miranda16ea1b12017-12-12 14:52:48 -080046
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 */
51public 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 */
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 */
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 */
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 */
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
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 */
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 */
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 */
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}