/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wallpaper.util; import static java.nio.charset.StandardCharsets.UTF_8; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; import android.util.Log; import com.android.wallpaper.compat.BuildCompat; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeUnit; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; /** * Logs messages to logcat and for debuggable build types ("eng" or "userdebug") also mirrors logs * to a disk-based log buffer. */ public class DiskBasedLogger { static final String LOGS_FILE_PATH = "logs.txt"; static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("EEE MMM dd HH:mm:ss.SSS z yyyy", Locale.US); private static final String TEMP_LOGS_FILE_PATH = "temp_logs.txt"; private static final String TAG = "DiskBasedLogger"; /** * POJO used to lock thread creation and file read/write operations. */ private static final Object S_LOCK = new Object(); private static final long THREAD_TIMEOUT_MILLIS = TimeUnit.MILLISECONDS.convert(2, TimeUnit.MINUTES); private static Handler sHandler; private static HandlerThread sLoggerThread; private static final Runnable THREAD_CLEANUP_RUNNABLE = new Runnable() { @Override public void run() { if (sLoggerThread != null && sLoggerThread.isAlive()) { // HandlerThread#quitSafely was added in JB-MR2, so prefer to use that instead of #quit. boolean isQuitSuccessful = BuildCompat.isAtLeastJBMR2() ? sLoggerThread.quitSafely() : sLoggerThread.quit(); if (!isQuitSuccessful) { Log.e(TAG, "Unable to quit disk-based logger HandlerThread"); } sLoggerThread = null; sHandler = null; } } }; /** * Initializes and returns a new dedicated HandlerThread for reading and writing to the disk-based * logs file. */ private static void initializeLoggerThread() { sLoggerThread = new HandlerThread("DiskBasedLoggerThread", Process.THREAD_PRIORITY_BACKGROUND); sLoggerThread.start(); } /** * Returns a Handler that can post messages to the dedicated HandlerThread for reading and writing * to the logs file on disk. Lazy-loads the HandlerThread if it doesn't already exist and delays * its death by a timeout if the thread already exists. */ private static Handler getLoggerThreadHandler() { synchronized (S_LOCK) { if (sLoggerThread == null) { initializeLoggerThread(); // Create a new Handler tied to the new HandlerThread's Looper for processing disk I/O off // the main thread. Starts with a default timeout to quit and remove references to the // thread after a period of inactivity. sHandler = new Handler(sLoggerThread.getLooper()); } else { sHandler.removeCallbacks(THREAD_CLEANUP_RUNNABLE); } // Delay the logger thread's eventual death. sHandler.postDelayed(THREAD_CLEANUP_RUNNABLE, THREAD_TIMEOUT_MILLIS); return sHandler; } } /** * Logs an "error" level log to logcat based on the provided tag and message and also duplicates * the log to a file-based log buffer if running on a "userdebug" or "eng" build. */ public static void e(String tag, String msg, Context context) { // Pass log tag and message through to logcat regardless of build type. Log.e(tag, msg); // Only mirror logs to disk-based log buffer if the build is debuggable. if (!Build.TYPE.equals("eng") && !Build.TYPE.equals("userdebug")) { return; } Handler handler = getLoggerThreadHandler(); if (handler == null) { Log.e(TAG, "Something went wrong creating the logger thread handler, quitting this logging " + "operation"); return; } handler.post(() -> { File logs = new File(context.getFilesDir(), LOGS_FILE_PATH); // Construct a log message that we can parse later in order to clean up old logs. String datetime = DATE_FORMAT.format(Calendar.getInstance().getTime()); String log = datetime + "/E " + tag + ": " + msg + "\n"; synchronized (S_LOCK) { FileOutputStream outputStream; try { outputStream = context.openFileOutput(logs.getName(), Context.MODE_APPEND); outputStream.write(log.getBytes(UTF_8)); outputStream.close(); } catch (IOException e) { Log.e(TAG, "Unable to close output stream for disk-based log buffer", e); } } }); } /** * Deletes logs in the disk-based log buffer older than 7 days. */ public static void clearOldLogs(Context context) { if (!Build.TYPE.equals("eng") && !Build.TYPE.equals("userdebug")) { return; } Handler handler = getLoggerThreadHandler(); if (handler == null) { Log.e(TAG, "Something went wrong creating the logger thread handler, quitting this logging " + "operation"); return; } handler.post(() -> { // Check if the logs file exists first before trying to read from it. File logsFile = new File(context.getFilesDir(), LOGS_FILE_PATH); if (!logsFile.exists()) { Log.w(TAG, "Disk-based log buffer doesn't exist, so there's nothing to clean up."); return; } synchronized (S_LOCK) { FileInputStream inputStream; BufferedReader bufferedReader; try { inputStream = context.openFileInput(LOGS_FILE_PATH); bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF_8)); } catch (IOException e) { Log.e(TAG, "IO exception opening a buffered reader for the existing logs file", e); return; } Date sevenDaysAgo = getSevenDaysAgo(); File tempLogsFile = new File(context.getFilesDir(), TEMP_LOGS_FILE_PATH); FileOutputStream outputStream; try { outputStream = context.openFileOutput(TEMP_LOGS_FILE_PATH, Context.MODE_APPEND); } catch (IOException e) { Log.e(TAG, "Unable to close output stream for disk-based log buffer", e); return; } copyLogsNewerThanDate(bufferedReader, outputStream, sevenDaysAgo); // Close streams to prevent resource leaks. closeStream(inputStream, "couldn't close input stream for log file"); closeStream(outputStream, "couldn't close output stream for temp log file"); // Rename temp log file (if it exists--which is only when the logs file has logs newer than // 7 days to begin with) to the final logs file. if (tempLogsFile.exists() && !tempLogsFile.renameTo(logsFile)) { Log.e(TAG, "couldn't rename temp logs file to final logs file"); } } }); } @Nullable @VisibleForTesting /* package */ static Handler getHandler() { return sHandler; } /** * Constructs and returns a {@link Date} object representing the time 7 days ago. */ private static Date getSevenDaysAgo() { Calendar sevenDaysAgoCalendar = Calendar.getInstance(); sevenDaysAgoCalendar.add(Calendar.DAY_OF_MONTH, -7); return sevenDaysAgoCalendar.getTime(); } /** * Tries to close the provided Closeable stream and logs the error message if the stream couldn't * be closed. */ private static void closeStream(Closeable stream, String errorMessage) { try { stream.close(); } catch (IOException e) { Log.e(TAG, errorMessage); } } /** * Copies all log lines newer than the supplied date from the provided {@link BufferedReader} to * the provided {@OutputStream}. *

* The caller of this method is responsible for closing the output stream and input stream * underlying the BufferedReader when all operations have finished. */ private static void copyLogsNewerThanDate(BufferedReader reader, OutputStream outputStream, Date date) { try { String line = reader.readLine(); while (line != null) { // Get the date from the line string. String datetime = line.split("/")[0]; Date logDate; try { logDate = DATE_FORMAT.parse(datetime); } catch (ParseException e) { Log.e(TAG, "Error parsing date from previous logs", e); return; } // Copy logs newer than the provided date into a temp log file. if (logDate.after(date)) { outputStream.write(line.getBytes(UTF_8)); outputStream.write("\n".getBytes(UTF_8)); } line = reader.readLine(); } } catch (IOException e) { Log.e(TAG, "IO exception while reading line from buffered reader", e); } } }