Files
termux-app/termux-shared/src/main/java/com/termux/shared/logger/Logger.java
agnostic-apollo 6bda7c4fc4 Added|Changed: Add Logger.logErrorPrivate*() functions which do not log errors that may contain potentially private info unless log level is debug or higher
Execution commands and other errors that may contain potentially private info should not be logged unless user has explicitly allowed it since apps with `READ_LOGS` permission would be able to read the data. A notification for failed executions commands would still be shown if enabled and required.
2022-04-23 00:36:12 +05:00

503 lines
18 KiB
Java

package com.termux.shared.logger;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Logger {
private static String DEFAULT_LOG_TAG = "Logger";
public static final int LOG_LEVEL_OFF = 0; // log nothing
public static final int LOG_LEVEL_NORMAL = 1; // start logging error, warn and info messages and stacktraces
public static final int LOG_LEVEL_DEBUG = 2; // start logging debug messages
public static final int LOG_LEVEL_VERBOSE = 3; // start logging verbose messages
public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL;
public static final int MAX_LOG_LEVEL = LOG_LEVEL_VERBOSE;
private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
/**
* The maximum size of the log entry payload that can be written to the logger. An attempt to
* write more than this amount will result in a truncated log entry.
*
* The limit is 4068 but this includes log tag and log level prefix "D/" before log tag and ": "
* suffix after it.
*
* #define LOGGER_ENTRY_MAX_PAYLOAD 4068
* https://cs.android.com/android/_/android/platform/system/core/+/android10-release:liblog/include/log/log_read.h;l=127
*/
public static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068; // 4068 bytes
/**
* The maximum safe size of the log entry payload that can be written to the logger, based on
* {@link #LOGGER_ENTRY_MAX_PAYLOAD}. Using 4000 as a safe limit to give log tag and its
* prefix/suffix max 68 characters for itself. Use "log*Extended()" functions to use max possible
* limit if tag is already known.
*/
public static final int LOGGER_ENTRY_MAX_SAFE_PAYLOAD = 4000; // 4000 bytes
public static void logMessage(int logPriority, String tag, String message) {
if (logPriority == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.e(getFullTag(tag), message);
else if (logPriority == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.w(getFullTag(tag), message);
else if (logPriority == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.i(getFullTag(tag), message);
else if (logPriority == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
Log.d(getFullTag(tag), message);
else if (logPriority == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE)
Log.v(getFullTag(tag), message);
}
public static void logExtendedMessage(int logLevel, String tag, String message) {
if (message == null) return;
int cutOffIndex;
int nextNewlineIndex;
String prefix = "";
// -8 for prefix "(xx/xx)" (max 99 sections), - log tag length, -4 for log tag prefix "D/" and suffix ": "
int maxEntrySize = LOGGER_ENTRY_MAX_PAYLOAD - 8 - getFullTag(tag).length() - 4;
List<String> messagesList = new ArrayList<>();
while(!message.isEmpty()) {
if (message.length() > maxEntrySize) {
cutOffIndex = maxEntrySize;
nextNewlineIndex = message.lastIndexOf('\n', cutOffIndex);
if (nextNewlineIndex != -1) {
cutOffIndex = nextNewlineIndex + 1;
}
messagesList.add(message.substring(0, cutOffIndex));
message = message.substring(cutOffIndex);
} else {
messagesList.add(message);
break;
}
}
for(int i=0; i<messagesList.size(); i++) {
if (messagesList.size() > 1)
prefix = "(" + (i + 1) + "/" + messagesList.size() + ")\n";
logMessage(logLevel, tag, prefix + messagesList.get(i));
}
}
public static void logError(String tag, String message) {
logMessage(Log.ERROR, tag, message);
}
public static void logError(String message) {
logMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
}
public static void logErrorExtended(String tag, String message) {
logExtendedMessage(Log.ERROR, tag, message);
}
public static void logErrorExtended(String message) {
logExtendedMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
}
public static void logErrorPrivate(String tag, String message) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
logMessage(Log.ERROR, tag, message);
}
public static void logErrorPrivate(String message) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
logMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
}
public static void logErrorPrivateExtended(String tag, String message) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
logExtendedMessage(Log.ERROR, tag, message);
}
public static void logErrorPrivateExtended(String message) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
logExtendedMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
}
public static void logWarn(String tag, String message) {
logMessage(Log.WARN, tag, message);
}
public static void logWarn(String message) {
logMessage(Log.WARN, DEFAULT_LOG_TAG, message);
}
public static void logWarnExtended(String tag, String message) {
logExtendedMessage(Log.WARN, tag, message);
}
public static void logWarnExtended(String message) {
logExtendedMessage(Log.WARN, DEFAULT_LOG_TAG, message);
}
public static void logInfo(String tag, String message) {
logMessage(Log.INFO, tag, message);
}
public static void logInfo(String message) {
logMessage(Log.INFO, DEFAULT_LOG_TAG, message);
}
public static void logInfoExtended(String tag, String message) {
logExtendedMessage(Log.INFO, tag, message);
}
public static void logInfoExtended(String message) {
logExtendedMessage(Log.INFO, DEFAULT_LOG_TAG, message);
}
public static void logDebug(String tag, String message) {
logMessage(Log.DEBUG, tag, message);
}
public static void logDebug(String message) {
logMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
}
public static void logDebugExtended(String tag, String message) {
logExtendedMessage(Log.DEBUG, tag, message);
}
public static void logDebugExtended(String message) {
logExtendedMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
}
public static void logVerbose(String tag, String message) {
logMessage(Log.VERBOSE, tag, message);
}
public static void logVerbose(String message) {
logMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
}
public static void logVerboseExtended(String tag, String message) {
logExtendedMessage(Log.VERBOSE, tag, message);
}
public static void logVerboseExtended(String message) {
logExtendedMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
}
public static void logVerboseForce(String tag, String message) {
Log.v(tag, message);
}
public static void logInfoAndShowToast(Context context, String tag, String message) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) {
logInfo(tag, message);
showToast(context, message, true);
}
}
public static void logInfoAndShowToast(Context context, String message) {
logInfoAndShowToast(context, DEFAULT_LOG_TAG, message);
}
public static void logErrorAndShowToast(Context context, String tag, String message) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) {
logError(tag, message);
showToast(context, message, true);
}
}
public static void logErrorAndShowToast(Context context, String message) {
logErrorAndShowToast(context, DEFAULT_LOG_TAG, message);
}
public static void logDebugAndShowToast(Context context, String tag, String message) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG) {
logDebug(tag, message);
showToast(context, message, true);
}
}
public static void logDebugAndShowToast(Context context, String message) {
logDebugAndShowToast(context, DEFAULT_LOG_TAG, message);
}
public static void logStackTraceWithMessage(String tag, String message, Throwable throwable) {
Logger.logErrorExtended(tag, getMessageAndStackTraceString(message, throwable));
}
public static void logStackTraceWithMessage(String message, Throwable throwable) {
logStackTraceWithMessage(DEFAULT_LOG_TAG, message, throwable);
}
public static void logStackTrace(String tag, Throwable throwable) {
logStackTraceWithMessage(tag, null, throwable);
}
public static void logStackTrace(Throwable throwable) {
logStackTraceWithMessage(DEFAULT_LOG_TAG, null, throwable);
}
public static void logStackTracesWithMessage(String tag, String message, List<Throwable> throwablesList) {
Logger.logErrorExtended(tag, getMessageAndStackTracesString(message, throwablesList));
}
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
if (message == null && throwable == null)
return null;
else if (message != null && throwable != null)
return message + ":\n" + getStackTraceString(throwable);
else if (throwable == null)
return message;
else
return getStackTraceString(throwable);
}
public static String getMessageAndStackTracesString(String message, List<Throwable> throwablesList) {
if (message == null && (throwablesList == null || throwablesList.size() == 0))
return null;
else if (message != null && (throwablesList != null && throwablesList.size() != 0))
return message + ":\n" + getStackTracesString(null, getStackTracesStringArray(throwablesList));
else if (throwablesList == null || throwablesList.size() == 0)
return message;
else
return getStackTracesString(null, getStackTracesStringArray(throwablesList));
}
public static String getStackTraceString(Throwable throwable) {
if (throwable == null) return null;
String stackTraceString = null;
try {
StringWriter errors = new StringWriter();
PrintWriter pw = new PrintWriter(errors);
throwable.printStackTrace(pw);
pw.close();
stackTraceString = errors.toString();
errors.close();
} catch (IOException e) {
e.printStackTrace();
}
return stackTraceString;
}
public static String[] getStackTracesStringArray(Throwable throwable) {
return getStackTracesStringArray(Collections.singletonList(throwable));
}
public static String[] getStackTracesStringArray(List<Throwable> throwablesList) {
if (throwablesList == null) return null;
final String[] stackTraceStringArray = new String[throwablesList.size()];
for (int i = 0; i < throwablesList.size(); i++) {
stackTraceStringArray[i] = getStackTraceString(throwablesList.get(i));
}
return stackTraceStringArray;
}
public static String getStackTracesString(String label, String[] stackTraceStringArray) {
if (label == null) label = "StackTraces:";
StringBuilder stackTracesString = new StringBuilder(label);
if (stackTraceStringArray == null || stackTraceStringArray.length == 0) {
stackTracesString.append(" -");
} else {
for (int i = 0; i != stackTraceStringArray.length; i++) {
if (stackTraceStringArray.length > 1)
stackTracesString.append("\n\nStacktrace ").append(i + 1);
stackTracesString.append("\n```\n").append(stackTraceStringArray[i]).append("\n```\n");
}
}
return stackTracesString.toString();
}
public static String getStackTracesMarkdownString(String label, String[] stackTraceStringArray) {
if (label == null) label = "StackTraces";
StringBuilder stackTracesString = new StringBuilder("### " + label);
if (stackTraceStringArray == null || stackTraceStringArray.length == 0) {
stackTracesString.append("\n\n`-`");
} else {
for (int i = 0; i != stackTraceStringArray.length; i++) {
if (stackTraceStringArray.length > 1)
stackTracesString.append("\n\n\n#### Stacktrace ").append(i + 1);
stackTracesString.append("\n\n```\n").append(stackTraceStringArray[i]).append("\n```");
}
}
stackTracesString.append("\n##\n");
return stackTracesString.toString();
}
public static String getSingleLineLogStringEntry(String label, Object object, String def) {
if (object != null)
return label + ": `" + object + "`";
else
return label + ": " + def;
}
public static String getMultiLineLogStringEntry(String label, Object object, String def) {
if (object != null)
return label + ":\n```\n" + object + "\n```\n";
else
return label + ": " + def;
}
public static void showToast(final Context context, final String toastText, boolean longDuration) {
if (context == null || DataUtils.isNullOrEmpty(toastText)) return;
new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, toastText, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show());
}
public static CharSequence[] getLogLevelsArray() {
return new CharSequence[]{
String.valueOf(LOG_LEVEL_OFF),
String.valueOf(LOG_LEVEL_NORMAL),
String.valueOf(LOG_LEVEL_DEBUG),
String.valueOf(LOG_LEVEL_VERBOSE)
};
}
public static CharSequence[] getLogLevelLabelsArray(Context context, CharSequence[] logLevels, boolean addDefaultTag) {
if (logLevels == null) return null;
CharSequence[] logLevelLabels = new CharSequence[logLevels.length];
for(int i=0; i<logLevels.length; i++) {
logLevelLabels[i] = getLogLevelLabel(context, Integer.parseInt(logLevels[i].toString()), addDefaultTag);
}
return logLevelLabels;
}
public static String getLogLevelLabel(final Context context, final int logLevel, final boolean addDefaultTag) {
String logLabel;
switch (logLevel) {
case LOG_LEVEL_OFF: logLabel = context.getString(R.string.log_level_off); break;
case LOG_LEVEL_NORMAL: logLabel = context.getString(R.string.log_level_normal); break;
case LOG_LEVEL_DEBUG: logLabel = context.getString(R.string.log_level_debug); break;
case LOG_LEVEL_VERBOSE: logLabel = context.getString(R.string.log_level_verbose); break;
default: logLabel = context.getString(R.string.log_level_unknown); break;
}
if (addDefaultTag && logLevel == DEFAULT_LOG_LEVEL)
return logLabel + " (default)";
else
return logLabel;
}
@NonNull
public static String getDefaultLogTag() {
return DEFAULT_LOG_TAG;
}
/**
* IllegalArgumentException will be thrown if tag.length() > 23 for Nougat (7.0) and prior releases.
* https://developer.android.com/reference/android/util/Log#isLoggable(java.lang.String,%20int) */
public static void setDefaultLogTag(@NonNull String defaultLogTag) {
DEFAULT_LOG_TAG = defaultLogTag.length() >= 23 ? defaultLogTag.substring(0, 22) : defaultLogTag;
}
public static int getLogLevel() {
return CURRENT_LOG_LEVEL;
}
public static int setLogLevel(Context context, int logLevel) {
if (isLogLevelValid(logLevel))
CURRENT_LOG_LEVEL = logLevel;
else
CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
if (context != null)
showToast(context, context.getString(R.string.log_level_value, getLogLevelLabel(context, CURRENT_LOG_LEVEL, false)),true);
return CURRENT_LOG_LEVEL;
}
/** The colon character ":" must not exist inside the tag, otherwise the `logcat` command
* filterspecs arguments `<tag>[:priority]` will not work and will throw `Invalid filter expression`
* error.
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:system/logging/liblog/logprint.cpp;l=363
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:system/logging/logcat/logcat.cpp;l=884
* */
public static String getFullTag(String tag) {
if (DEFAULT_LOG_TAG.equals(tag))
return tag;
else
return DEFAULT_LOG_TAG + "." + tag;
}
public static boolean isLogLevelValid(Integer logLevel) {
return (logLevel != null && logLevel >= LOG_LEVEL_OFF && logLevel <= MAX_LOG_LEVEL);
}
/** Check if custom log level is valid and >= {@link #CURRENT_LOG_LEVEL}. If custom log level is
* not valid then {@link #LOG_LEVEL_VERBOSE} must be >= {@link #CURRENT_LOG_LEVEL}. */
public static boolean shouldEnableLoggingForCustomLogLevel(Integer customLogLevel) {
if (CURRENT_LOG_LEVEL <= LOG_LEVEL_OFF) return false;
if (customLogLevel == null) return CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE; // Use default app log level
if (customLogLevel <= LOG_LEVEL_OFF) return false;
customLogLevel = Logger.isLogLevelValid(customLogLevel) ? customLogLevel: Logger.LOG_LEVEL_VERBOSE;
return (customLogLevel >= CURRENT_LOG_LEVEL);
}
}