• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.annotation.Nullable;
28 import androidx.annotation.VisibleForTesting;
29 
30 import java.io.BufferedReader;
31 import java.io.Closeable;
32 import java.io.File;
33 import java.io.FileInputStream;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.InputStreamReader;
37 import java.io.OutputStream;
38 import java.text.ParseException;
39 import java.text.SimpleDateFormat;
40 import java.util.Calendar;
41 import java.util.Date;
42 import java.util.Locale;
43 import java.util.concurrent.TimeUnit;
44 
45 /**
46  * Logs messages to logcat and for debuggable build types ("eng" or "userdebug") also mirrors logs
47  * to a disk-based log buffer.
48  */
49 public class DiskBasedLogger {
50 
51     static final String LOGS_FILE_PATH = "logs.txt";
52     static final SimpleDateFormat DATE_FORMAT =
53             new SimpleDateFormat("EEE MMM dd HH:mm:ss.SSS z yyyy", Locale.US);
54 
55     private static final String TEMP_LOGS_FILE_PATH = "temp_logs.txt";
56     private static final String TAG = "DiskBasedLogger";
57 
58     /**
59      * POJO used to lock thread creation and file read/write operations.
60      */
61     private static final Object S_LOCK = new Object();
62 
63     private static final long THREAD_TIMEOUT_MILLIS =
64             TimeUnit.MILLISECONDS.convert(2, TimeUnit.MINUTES);
65     private static Handler sHandler;
66     private static HandlerThread sLoggerThread;
67     private static final Runnable THREAD_CLEANUP_RUNNABLE = new Runnable() {
68         @Override
69         public void run() {
70             if (sLoggerThread != null && sLoggerThread.isAlive()) {
71 
72                 // HandlerThread#quitSafely was added in JB-MR2, so prefer to use that instead of #quit.
73                 boolean isQuitSuccessful =
74                         Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
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      */
initializeLoggerThread()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      */
getLoggerThreadHandler()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      */
e(String tag, String msg, Context context)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      */
clearOldLogs(Context context)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
getHandler()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      */
getSevenDaysAgo()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      */
closeStream(Closeable stream, String errorMessage)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      */
copyLogsNewerThanDate(BufferedReader reader, OutputStream outputStream, Date date)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 }
287