1 /* 2 * Copyright (C) 2024 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 android.telephony; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.os.DropBoxManager; 23 import android.os.Handler; 24 import android.os.HandlerThread; 25 import android.util.Log; 26 27 import com.android.internal.R; 28 import com.android.internal.annotations.GuardedBy; 29 30 import java.time.Instant; 31 import java.time.ZoneId; 32 import java.time.format.DateTimeFormatter; 33 import java.util.Optional; 34 35 /** 36 * A persistent logger backend that stores logs in Android DropBoxManager 37 * 38 * @hide 39 */ 40 public class DropBoxManagerLoggerBackend implements PersistentLoggerBackend { 41 42 private static final String TAG = "DropBoxManagerLoggerBackend"; 43 // Separate tag reference to be explicitly used for dropboxmanager instead of logcat logging 44 private static final String DROPBOX_TAG = "DropBoxManagerLoggerBackend"; 45 private static final DateTimeFormatter LOG_TIMESTAMP_FORMATTER = 46 DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS"); 47 private static final ZoneId LOCAL_ZONE_ID = ZoneId.systemDefault(); 48 private static final int BUFFER_SIZE_BYTES = 500 * 1024; // 500 KB 49 private static final int MIN_BUFFER_BYTES_FOR_FLUSH = 5 * 1024; // 5 KB 50 51 private static DropBoxManagerLoggerBackend sInstance; 52 53 private final DropBoxManager mDropBoxManager; 54 private final Object mBufferLock = new Object(); 55 @GuardedBy("mBufferLock") 56 private final StringBuilder mLogBuffer = new StringBuilder(); 57 private long mBufferStartTime = -1L; 58 private final HandlerThread mHandlerThread = new HandlerThread(DROPBOX_TAG); 59 private final Handler mHandler; 60 // Flag for determining if logging is enabled as a general feature 61 private final boolean mDropBoxManagerLoggingEnabled; 62 // Flag for controlling if logging is enabled at runtime 63 private boolean mIsLoggingEnabled = false; 64 65 /** 66 * Returns a singleton instance of {@code DropBoxManagerLoggerBackend} that will log to 67 * DropBoxManager if the config_dropboxmanager_persistent_logging_enabled resource config is 68 * enabled. 69 * @param context Android context 70 */ 71 @Nullable getInstance(@onNull Context context)72 public static synchronized DropBoxManagerLoggerBackend getInstance(@NonNull Context context) { 73 if (sInstance == null) { 74 sInstance = new DropBoxManagerLoggerBackend(context); 75 } 76 return sInstance; 77 } 78 DropBoxManagerLoggerBackend(@onNull Context context)79 private DropBoxManagerLoggerBackend(@NonNull Context context) { 80 mDropBoxManager = context.getSystemService(DropBoxManager.class); 81 mHandlerThread.start(); 82 mHandler = new Handler(mHandlerThread.getLooper()); 83 mDropBoxManagerLoggingEnabled = persistentLoggingEnabled(context); 84 } 85 persistentLoggingEnabled(@onNull Context context)86 private boolean persistentLoggingEnabled(@NonNull Context context) { 87 try { 88 return context.getResources().getBoolean( 89 R.bool.config_dropboxmanager_persistent_logging_enabled); 90 } catch (RuntimeException e) { 91 Log.w(TAG, "Persistent logging config not found"); 92 return false; 93 } 94 } 95 96 /** 97 * Enable or disable logging to DropBoxManager 98 * @param isLoggingEnabled Whether logging should be enabled 99 */ setLoggingEnabled(boolean isLoggingEnabled)100 public void setLoggingEnabled(boolean isLoggingEnabled) { 101 Log.i(DROPBOX_TAG, "toggle logging: " + isLoggingEnabled); 102 mIsLoggingEnabled = isLoggingEnabled; 103 } 104 105 /** 106 * Persist a DEBUG log message. 107 * @param tag Used to identify the source of a log message. 108 * @param msg The message you would like logged. 109 */ debug(@onNull String tag, @NonNull String msg)110 public void debug(@NonNull String tag, @NonNull String msg) { 111 if (!mDropBoxManagerLoggingEnabled) { 112 return; 113 } 114 bufferLog("D", tag, msg, Optional.empty()); 115 } 116 117 /** 118 * Persist a INFO log message. 119 * @param tag Used to identify the source of a log message. 120 * @param msg The message you would like logged. 121 */ info(@onNull String tag, @NonNull String msg)122 public void info(@NonNull String tag, @NonNull String msg) { 123 if (!mDropBoxManagerLoggingEnabled) { 124 return; 125 } 126 bufferLog("I", tag, msg, Optional.empty()); 127 } 128 129 /** 130 * Persist a WARN log message. 131 * @param tag Used to identify the source of a log message. 132 * @param msg The message you would like logged. 133 */ warn(@onNull String tag, @NonNull String msg)134 public void warn(@NonNull String tag, @NonNull String msg) { 135 if (!mDropBoxManagerLoggingEnabled) { 136 return; 137 } 138 bufferLog("W", tag, msg, Optional.empty()); 139 } 140 141 /** 142 * Persist a WARN log message. 143 * @param tag Used to identify the source of a log message. 144 * @param msg The message you would like logged. 145 * @param t An exception to log. 146 */ warn(@onNull String tag, @NonNull String msg, @NonNull Throwable t)147 public void warn(@NonNull String tag, @NonNull String msg, @NonNull Throwable t) { 148 if (!mDropBoxManagerLoggingEnabled) { 149 return; 150 } 151 bufferLog("W", tag, msg, Optional.of(t)); 152 } 153 154 /** 155 * Persist a ERROR log message. 156 * @param tag Used to identify the source of a log message. 157 * @param msg The message you would like logged. 158 */ error(@onNull String tag, @NonNull String msg)159 public void error(@NonNull String tag, @NonNull String msg) { 160 if (!mDropBoxManagerLoggingEnabled) { 161 return; 162 } 163 bufferLog("E", tag, msg, Optional.empty()); 164 } 165 166 /** 167 * Persist a ERROR log message. 168 * @param tag Used to identify the source of a log message. 169 * @param msg The message you would like logged. 170 * @param t An exception to log. 171 */ error(@onNull String tag, @NonNull String msg, @NonNull Throwable t)172 public void error(@NonNull String tag, @NonNull String msg, @NonNull Throwable t) { 173 if (!mDropBoxManagerLoggingEnabled) { 174 return; 175 } 176 bufferLog("E", tag, msg, Optional.of(t)); 177 } 178 bufferLog( @onNull String level, @NonNull String tag, @NonNull String msg, Optional<Throwable> t)179 private synchronized void bufferLog( 180 @NonNull String level, 181 @NonNull String tag, 182 @NonNull String msg, 183 Optional<Throwable> t) { 184 if (!mIsLoggingEnabled) { 185 return; 186 } 187 188 if (mBufferStartTime == -1L) { 189 mBufferStartTime = System.currentTimeMillis(); 190 } 191 192 synchronized (mBufferLock) { 193 mLogBuffer 194 .append(formatLog(level, tag, msg, t)) 195 .append("\n"); 196 197 if (mLogBuffer.length() >= BUFFER_SIZE_BYTES) { 198 flushAsync(); 199 } 200 } 201 } 202 formatLog( @onNull String level, @NonNull String tag, @NonNull String msg, Optional<Throwable> t)203 private String formatLog( 204 @NonNull String level, 205 @NonNull String tag, 206 @NonNull String msg, 207 Optional<Throwable> t) { 208 // Expected format = "$Timestamp $Level $Tag: $Message" 209 return formatTimestamp(System.currentTimeMillis()) + " " + level + " " + tag + ": " 210 + t.map(throwable -> msg + ": " + Log.getStackTraceString(throwable)).orElse(msg); 211 } 212 formatTimestamp(long currentTimeMillis)213 private String formatTimestamp(long currentTimeMillis) { 214 return Instant.ofEpochMilli(currentTimeMillis) 215 .atZone(LOCAL_ZONE_ID) 216 .format(LOG_TIMESTAMP_FORMATTER); 217 } 218 219 /** 220 * Flushes all buffered logs into DropBoxManager as a single log record with a tag of 221 * {@link #DROPBOX_TAG} asynchronously. Should be invoked sparingly as DropBoxManager has 222 * device-level limitations on the number files that can be stored. 223 */ flushAsync()224 public void flushAsync() { 225 if (!mDropBoxManagerLoggingEnabled) { 226 return; 227 } 228 229 mHandler.post(this::flush); 230 }; 231 232 /** 233 * Flushes all buffered logs into DropBoxManager as a single log record with a tag of 234 * {@link #DROPBOX_TAG}. Should be invoked sparingly as DropBoxManager has device-level 235 * limitations on the number files that can be stored. 236 */ flush()237 public void flush() { 238 if (!mDropBoxManagerLoggingEnabled) { 239 return; 240 } 241 242 synchronized (mBufferLock) { 243 if (mLogBuffer.length() < MIN_BUFFER_BYTES_FOR_FLUSH) { 244 return; 245 } 246 247 Log.d(DROPBOX_TAG, "Flushing logs from " 248 + formatTimestamp(mBufferStartTime) + " to " 249 + formatTimestamp(System.currentTimeMillis())); 250 251 try { 252 mDropBoxManager.addText(DROPBOX_TAG, mLogBuffer.toString()); 253 } catch (Exception e) { 254 Log.w(DROPBOX_TAG, "Failed to flush logs of length " 255 + mLogBuffer.length() + " to DropBoxManager", e); 256 } 257 mLogBuffer.setLength(0); 258 } 259 mBufferStartTime = -1L; 260 } 261 } 262