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