diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java
index 9df38ac99..83d0c7358 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java
@@ -1,15 +1,26 @@
package app.revanced.extension.shared;
+import static app.revanced.extension.shared.settings.BaseSettings.DEBUG;
+import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE;
+import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
+
import android.util.Log;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import app.revanced.extension.shared.settings.BaseSettings;
import java.io.PrintWriter;
import java.io.StringWriter;
-import static app.revanced.extension.shared.settings.BaseSettings.*;
+import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.shared.settings.preference.LogBufferManager;
+/**
+ * ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
+ * and additionally accessible thru {@link LogBufferManager}.
+ *
+ * All methods are thread safe.
+ */
public class Logger {
/**
@@ -17,99 +28,158 @@ public class Logger {
*/
@FunctionalInterface
public interface LogMessage {
+ /**
+ * @return Logger string message. This method is only called if logging is enabled.
+ */
@NonNull
String buildMessageString();
+ }
- /**
- * @return For outer classes, this returns {@link Class#getSimpleName()}.
- * For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
- *
- * For example, each of these classes return 'SomethingView':
- *
- * com.company.SomethingView
- * com.company.SomethingView$StaticClass
- * com.company.SomethingView$1
- *
- */
- private String findOuterClassSimpleName() {
- var selfClass = this.getClass();
+ private enum LogLevel {
+ DEBUG,
+ INFO,
+ ERROR
+ }
- String fullClassName = selfClass.getName();
- final int dollarSignIndex = fullClassName.indexOf('$');
- if (dollarSignIndex < 0) {
- return selfClass.getSimpleName(); // Already an outer class.
+ private static final String REVANCED_LOG_TAG = "revanced";
+
+ private static final String LOGGER_CLASS_NAME = Logger.class.getName();
+
+ /**
+ * @return For outer classes, this returns {@link Class#getSimpleName()}.
+ * For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
+ *
+ * For example, each of these classes returns 'SomethingView':
+ *
+ * com.company.SomethingView
+ * com.company.SomethingView$StaticClass
+ * com.company.SomethingView$1
+ *
+ */
+ private static String getOuterClassSimpleName(Object obj) {
+ Class> logClass = obj.getClass();
+ String fullClassName = logClass.getName();
+ final int dollarSignIndex = fullClassName.indexOf('$');
+ if (dollarSignIndex < 0) {
+ return logClass.getSimpleName(); // Already an outer class.
+ }
+
+ // Class is inner, static, or anonymous.
+ // Parse the simple name full name.
+ // A class with no package returns index of -1, but incrementing gives index zero which is correct.
+ final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
+ return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
+ }
+
+ /**
+ * Internal method to handle logging to Android Log and {@link LogBufferManager}.
+ * Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer
+ * with class name but without 'revanced:' prefix.
+ *
+ * @param logLevel The log level.
+ * @param message Log message object.
+ * @param ex Optional exception.
+ * @param includeStackTrace If the current stack should be included.
+ * @param showToast If a toast is to be shown.
+ */
+ private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex,
+ boolean includeStackTrace, boolean showToast) {
+ // It's very important that no Settings are used in this method,
+ // as this code is used when a context is not set and thus referencing
+ // a setting will crash the app.
+ String messageString = message.buildMessageString();
+ String className = getOuterClassSimpleName(message);
+
+ StringBuilder logBuilder = new StringBuilder(className.length() + 2
+ + messageString.length());
+ logBuilder.append(className).append(": ").append(messageString);
+
+ String toastMessage = showToast ? logBuilder.toString() : null;
+
+ // Append exception message if present.
+ if (ex != null) {
+ var exceptionMessage = ex.getMessage();
+ if (exceptionMessage != null) {
+ logBuilder.append("\nException: ").append(exceptionMessage);
}
+ }
- // Class is inner, static, or anonymous.
- // Parse the simple name full name.
- // A class with no package returns index of -1, but incrementing gives index zero which is correct.
- final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
- return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
+ if (includeStackTrace) {
+ var sw = new StringWriter();
+ new Throwable().printStackTrace(new PrintWriter(sw));
+ String stackTrace = sw.toString();
+ // Remove the stacktrace elements of this class.
+ final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME);
+ final int loggerBegins = stackTrace.indexOf('\n', loggerIndex);
+ logBuilder.append(stackTrace, loggerBegins, stackTrace.length());
+ }
+
+ String logText = logBuilder.toString();
+ LogBufferManager.appendToLogBuffer(logText);
+
+ switch (logLevel) {
+ case DEBUG:
+ if (ex == null) Log.d(REVANCED_LOG_TAG, logText);
+ else Log.d(REVANCED_LOG_TAG, logText, ex);
+ break;
+ case INFO:
+ if (ex == null) Log.i(REVANCED_LOG_TAG, logText);
+ else Log.i(REVANCED_LOG_TAG, logText, ex);
+ break;
+ case ERROR:
+ if (ex == null) Log.e(REVANCED_LOG_TAG, logText);
+ else Log.e(REVANCED_LOG_TAG, logText, ex);
+ break;
+ }
+
+ if (toastMessage != null) {
+ Utils.showToastLong(toastMessage);
}
}
- private static final String REVANCED_LOG_PREFIX = "revanced: ";
-
/**
* Logs debug messages under the outer class name of the code calling this method.
- * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
- * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
+ *
+ * Whenever possible, the log string should be constructed entirely inside + * {@link LogMessage#buildMessageString()} so the performance cost of + * building strings is paid only if {@link BaseSettings#DEBUG} is enabled. */ - public static void printDebug(@NonNull LogMessage message) { + public static void printDebug(LogMessage message) { printDebug(message, null); } /** * Logs debug messages under the outer class name of the code calling this method. - * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} - * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled. + *
+ * Whenever possible, the log string should be constructed entirely inside
+ * {@link LogMessage#buildMessageString()} so the performance cost of
+ * building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
- public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
+ public static void printDebug(LogMessage message, @Nullable Exception ex) {
if (DEBUG.get()) {
- String logMessage = message.buildMessageString();
- String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
-
- if (DEBUG_STACKTRACE.get()) {
- var builder = new StringBuilder(logMessage);
- var sw = new StringWriter();
- new Throwable().printStackTrace(new PrintWriter(sw));
-
- builder.append('\n').append(sw);
- logMessage = builder.toString();
- }
-
- if (ex == null) {
- Log.d(logTag, logMessage);
- } else {
- Log.d(logTag, logMessage, ex);
- }
+ logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false);
}
}
/**
* Logs information messages using the outer class name of the code calling this method.
*/
- public static void printInfo(@NonNull LogMessage message) {
+ public static void printInfo(LogMessage message) {
printInfo(message, null);
}
/**
* Logs information messages using the outer class name of the code calling this method.
*/
- public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
- String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
- String logMessage = message.buildMessageString();
- if (ex == null) {
- Log.i(logTag, logMessage);
- } else {
- Log.i(logTag, logMessage, ex);
- }
+ public static void printInfo(LogMessage message, @Nullable Exception ex) {
+ logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false);
}
/**
* Logs exceptions under the outer class name of the code calling this method.
+ * Appends the log message, exception (if present), and toast message (if enabled) to logBuffer.
*/
- public static void printException(@NonNull LogMessage message) {
+ public static void printException(LogMessage message) {
printException(message, null);
}
@@ -122,35 +192,23 @@ public class Logger {
* @param message log message
* @param ex exception (optional)
*/
- public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
- String messageString = message.buildMessageString();
- String outerClassSimpleName = message.findOuterClassSimpleName();
- String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
- if (ex == null) {
- Log.e(logMessage, messageString);
- } else {
- Log.e(logMessage, messageString, ex);
- }
- if (DEBUG_TOAST_ON_ERROR.get()) {
- Utils.showToastLong(outerClassSimpleName + ": " + messageString);
- }
+ public static void printException(LogMessage message, @Nullable Throwable ex) {
+ logInternal(LogLevel.ERROR, message, ex, DEBUG_STACKTRACE.get(), DEBUG_TOAST_ON_ERROR.get());
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
- public static void initializationInfo(@NonNull Class> callingClass, @NonNull String message) {
- Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
+ public static void initializationInfo(LogMessage message) {
+ logInternal(LogLevel.INFO, message, null, false, false);
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
- public static void initializationException(@NonNull Class> callingClass, @NonNull String message,
- @Nullable Exception ex) {
- Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
+ public static void initializationException(LogMessage message, @Nullable Exception ex) {
+ logInternal(LogLevel.ERROR, message, ex, false, false);
}
-
-}
\ No newline at end of file
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
index dead738b4..111a0dd77 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
@@ -363,15 +363,17 @@ public class Utils {
public static Context getContext() {
if (context == null) {
- Logger.initializationException(Utils.class, "Context is not set by extension hook, returning null", null);
+ Logger.initializationException(() -> "Context is not set by extension hook, returning null", null);
}
return context;
}
public static void setContext(Context appContext) {
+ // Intentionally use logger before context is set,
+ // to expose any bugs in the 'no context available' logger method.
+ Logger.initializationInfo(() -> "Set context: " + appContext);
// Must initially set context to check the app language.
context = appContext;
- Logger.initializationInfo(Utils.class, "Set context: " + appContext);
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
if (language != AppLanguage.DEFAULT) {
@@ -383,8 +385,9 @@ public class Utils {
}
}
- public static void setClipboard(@NonNull String text) {
- android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ public static void setClipboard(CharSequence text) {
+ android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context
+ .getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
clipboard.setPrimaryClip(clip);
}
@@ -548,14 +551,15 @@ public class Utils {
private static void showToast(@NonNull String messageToToast, int toastDuration) {
Objects.requireNonNull(messageToToast);
runOnMainThreadNowOrLater(() -> {
- if (context == null) {
- Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
- } else {
- Logger.printDebug(() -> "Showing toast: " + messageToToast);
- Toast.makeText(context, messageToToast, toastDuration).show();
- }
- }
- );
+ Context currentContext = context;
+
+ if (currentContext == null) {
+ Logger.initializationException(() -> "Cannot show toast (context is null): " + messageToToast, null);
+ } else {
+ Logger.printDebug(() -> "Showing toast: " + messageToToast);
+ Toast.makeText(currentContext, messageToToast, toastDuration).show();
+ }
+ });
}
public static boolean isDarkModeEnabled() {
@@ -579,7 +583,7 @@ public class Utils {
}
/**
- * Automatically logs any exceptions the runnable throws
+ * Automatically logs any exceptions the runnable throws.
*/
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
Runnable loggingRunnable = () -> {
@@ -605,14 +609,14 @@ public class Utils {
}
/**
- * @return if the calling thread is on the main thread
+ * @return if the calling thread is on the main thread.
*/
public static boolean isCurrentlyOnMainThread() {
return Looper.getMainLooper().isCurrentThread();
}
/**
- * @throws IllegalStateException if the calling thread is _off_ the main thread
+ * @throws IllegalStateException if the calling thread is _off_ the main thread.
*/
public static void verifyOnMainThread() throws IllegalStateException {
if (!isCurrentlyOnMainThread()) {
@@ -621,7 +625,7 @@ public class Utils {
}
/**
- * @throws IllegalStateException if the calling thread is _on_ the main thread
+ * @throws IllegalStateException if the calling thread is _on_ the main thread.
*/
public static void verifyOffMainThread() throws IllegalStateException {
if (isCurrentlyOnMainThread()) {
@@ -635,6 +639,11 @@ public class Utils {
OTHER,
}
+ /**
+ * Calling extension code must ensure the un-patched app has the permission
+ * android.permission.ACCESS_NETWORK_STATE
, otherwise the app will crash
+ * if this method is used.
+ */
public static boolean isNetworkConnected() {
NetworkType networkType = getNetworkType();
return networkType == NetworkType.MOBILE
@@ -642,10 +651,11 @@ public class Utils {
}
/**
- * Calling extension code must ensure the target app has the
- * ACCESS_NETWORK_STATE
app manifest permission.
+ * Calling extension code must ensure the un-patched app has the permission
+ * android.permission.ACCESS_NETWORK_STATE
, otherwise the app will crash
+ * if this method is used.
*/
- @SuppressWarnings({"deprecation", "MissingPermission"})
+ @SuppressLint({"MissingPermission", "deprecation"})
public static NetworkType getNetworkType() {
Context networkContext = getContext();
if (networkContext == null) {
@@ -782,8 +792,9 @@ public class Utils {
preferences.add(new Pair<>(sortValue, preference));
}
+ //noinspection ComparatorCombinators
Collections.sort(preferences, (pair1, pair2)
- -> pair1.first.compareToIgnoreCase(pair2.first));
+ -> pair1.first.compareTo(pair2.first));
int index = 0;
for (Pair