1 /* 2 * Copyright (C) 2019 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 17 package com.android.providers.media.util; 18 19 import static java.nio.file.StandardOpenOption.APPEND; 20 import static java.nio.file.StandardOpenOption.CREATE; 21 22 import android.os.SystemProperties; 23 import android.text.format.DateUtils; 24 import android.util.Log; 25 26 import androidx.annotation.GuardedBy; 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 30 import java.io.File; 31 import java.io.IOException; 32 import java.io.PrintWriter; 33 import java.io.Writer; 34 import java.nio.file.Files; 35 import java.nio.file.Path; 36 import java.text.SimpleDateFormat; 37 import java.util.Comparator; 38 import java.util.Date; 39 import java.util.Optional; 40 import java.util.stream.Stream; 41 42 public class Logging { 43 public static final String TAG = "MediaProvider"; 44 public static final boolean LOGW = Log.isLoggable(TAG, Log.WARN); 45 public static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); 46 public static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE); 47 48 public static final boolean IS_DEBUGGABLE = 49 SystemProperties.getInt("ro.debuggable", 0) == 1; 50 51 /** Size limit of each persistent log file, in bytes */ 52 private static final int PERSISTENT_SIZE = 32 * 1024; 53 private static final int PERSISTENT_COUNT = 4; 54 private static final long PERSISTENT_AGE = DateUtils.WEEK_IN_MILLIS; 55 private static final SimpleDateFormat DATE_FORMAT = 56 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 57 private static final Object LOCK = new Object(); 58 59 @GuardedBy("LOCK") 60 private static Path sPersistentDir; 61 @GuardedBy("LOCK") 62 private static Path sPersistentFile; 63 @GuardedBy("LOCK") 64 private static Writer sWriter; 65 66 /** 67 * Initialize persistent logging which is then available through 68 * {@link #logPersistent(String)} and {@link #dumpPersistent(PrintWriter)}. 69 */ initPersistent(@onNull File persistentDir)70 public static void initPersistent(@NonNull File persistentDir) { 71 synchronized (LOCK) { 72 sPersistentDir = persistentDir.toPath(); 73 closeWriterAndUpdatePathLocked(null); 74 } 75 } 76 77 /** 78 * Write the given message to persistent logs. 79 */ logPersistent(@onNull String msg)80 public static void logPersistent(@NonNull String msg) { 81 Log.i(TAG, msg); 82 83 synchronized (LOCK) { 84 if (sPersistentDir == null) return; 85 86 try { 87 Path path = resolveCurrentPersistentFileLocked(); 88 if (!path.equals(sPersistentFile)) { 89 closeWriterAndUpdatePathLocked(path); 90 } 91 92 if (sWriter == null) { 93 sWriter = Files.newBufferedWriter(path, CREATE, APPEND); 94 } 95 96 sWriter.write(DATE_FORMAT.format(new Date()) + " " + msg + "\n"); 97 // Flush to guarantee that all our writes have been sent to the filesystem 98 sWriter.flush(); 99 } catch (IOException e) { 100 closeWriterAndUpdatePathLocked(null); 101 Log.w(TAG, "Failed to write: " + sPersistentFile, e); 102 } 103 } 104 } 105 106 @GuardedBy("LOCK") closeWriterAndUpdatePathLocked(@ullable Path newPath)107 private static void closeWriterAndUpdatePathLocked(@Nullable Path newPath) { 108 if (sWriter != null) { 109 try { 110 sWriter.close(); 111 } catch (IOException ignored) { 112 Log.w(TAG, "Failed to close: " + sPersistentFile, ignored); 113 } 114 sWriter = null; 115 } 116 sPersistentFile = newPath; 117 } 118 119 /** 120 * Trim any persistent logs, typically called during idle maintenance. 121 */ trimPersistent()122 public static void trimPersistent() { 123 File persistentDir = null; 124 synchronized (LOCK) { 125 if (sPersistentDir == null) return; 126 persistentDir = sPersistentDir.toFile(); 127 128 closeWriterAndUpdatePathLocked(sPersistentFile); 129 } 130 131 FileUtils.deleteOlderFiles(persistentDir, PERSISTENT_COUNT, PERSISTENT_AGE); 132 } 133 134 /** 135 * Dump any persistent logs. 136 */ dumpPersistent(@onNull PrintWriter pw)137 public static void dumpPersistent(@NonNull PrintWriter pw) { 138 Path persistentDir = null; 139 synchronized (LOCK) { 140 if (sPersistentDir == null) return; 141 persistentDir = sPersistentDir; 142 } 143 144 try (Stream<Path> stream = Files.list(persistentDir)) { 145 stream.sorted().forEach((path) -> { 146 dumpPersistentFile(path, pw); 147 }); 148 } catch (IOException e) { 149 pw.println(e.getMessage()); 150 pw.println(); 151 } 152 } 153 dumpPersistentFile(@onNull Path path, @NonNull PrintWriter pw)154 private static void dumpPersistentFile(@NonNull Path path, @NonNull PrintWriter pw) { 155 pw.println("Persistent logs in " + path + ":"); 156 try (Stream<String> stream = Files.lines(path)) { 157 stream.forEach((line) -> { 158 pw.println(" " + line); 159 }); 160 pw.println(); 161 } catch (IOException e) { 162 pw.println(" " + e.getMessage()); 163 pw.println(); 164 } 165 } 166 167 /** 168 * Resolve the current log file to write new entries into. Automatically 169 * starts new files when the current file is larger than 170 * {@link #PERSISTENT_SIZE}. 171 */ 172 @GuardedBy("LOCK") resolveCurrentPersistentFileLocked()173 private static @NonNull Path resolveCurrentPersistentFileLocked() throws IOException { 174 if (sPersistentFile != null && sPersistentFile.toFile().length() < PERSISTENT_SIZE) { 175 return sPersistentFile; 176 } 177 178 return sPersistentDir.resolve(String.valueOf(System.currentTimeMillis())); 179 } 180 } 181