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