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