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 pair : preferences) { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java index a1d051c2b..20d2510e2 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java @@ -70,7 +70,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer // Show the user the settings in JSON format. builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> { - Utils.setClipboard(getEditText().getText().toString()); + Utils.setClipboard(getEditText().getText()); }).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> { importSettings(builder.getContext(), getEditText().getText().toString()); }); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java new file mode 100644 index 000000000..4bd54c65b --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java @@ -0,0 +1,113 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; + +import java.util.Deque; +import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicInteger; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BaseSettings; + +/** + * Manages a buffer for storing debug logs from {@link Logger}. + * Stores just under 1MB of the most recent log data. + * + * All methods are thread-safe. + */ +public final class LogBufferManager { + /** Maximum byte size of all buffer entries. Must be less than Android's 1 MB Binder transaction limit. */ + private static final int BUFFER_MAX_BYTES = 900_000; + /** Limit number of log lines. */ + private static final int BUFFER_MAX_SIZE = 10_000; + + private static final Deque logBuffer = new ConcurrentLinkedDeque<>(); + private static final AtomicInteger logBufferByteSize = new AtomicInteger(); + + /** + * Appends a log message to the internal buffer if debugging is enabled. + * The buffer is limited to approximately {@link #BUFFER_MAX_BYTES} or {@link #BUFFER_MAX_SIZE} + * to prevent excessive memory usage. + * + * @param message The log message to append. + */ + public static void appendToLogBuffer(String message) { + Objects.requireNonNull(message); + + // 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. + logBuffer.addLast(message); + int newSize = logBufferByteSize.addAndGet(message.length()); + + // Remove oldest entries if over the log size limits. + while (newSize > BUFFER_MAX_BYTES || logBuffer.size() > BUFFER_MAX_SIZE) { + String removed = logBuffer.pollFirst(); + if (removed == null) { + // Thread race of two different calls to this method, and the other thread won. + return; + } + + newSize = logBufferByteSize.addAndGet(-removed.length()); + } + } + + /** + * Exports all logs from the internal buffer to the clipboard. + * Displays a toast with the result. + */ + public static void exportToClipboard() { + try { + if (!BaseSettings.DEBUG.get()) { + Utils.showToastShort(str("revanced_debug_logs_disabled")); + return; + } + + if (logBuffer.isEmpty()) { + Utils.showToastShort(str("revanced_debug_logs_none_found")); + clearLogBufferData(); // Clear toast log entry that was just created. + return; + } + + // Most (but not all) Android 13+ devices always show a "copied to clipboard" toast + // and there is no way to programmatically detect if a toast will show or not. + // Show a toast even if using Android 13+, but show ReVanced toast first (before copying to clipboard). + Utils.showToastShort(str("revanced_debug_logs_copied_to_clipboard")); + + Utils.setClipboard(String.join("\n", logBuffer)); + } catch (Exception ex) { + // Handle security exception if clipboard access is denied. + String errorMessage = String.format(str("revanced_debug_logs_failed_to_export"), ex.getMessage()); + Utils.showToastLong(errorMessage); + Logger.printDebug(() -> errorMessage, ex); + } + } + + private static void clearLogBufferData() { + // Cannot simply clear the log buffer because there is no + // write lock for both the deque and the atomic int. + // Instead pop off log entries and decrement the size one by one. + while (!logBuffer.isEmpty()) { + String removed = logBuffer.pollFirst(); + if (removed != null) { + logBufferByteSize.addAndGet(-removed.length()); + } + } + } + + /** + * Clears the internal log buffer and displays a toast with the result. + */ + public static void clearLogBuffer() { + if (!BaseSettings.DEBUG.get()) { + Utils.showToastShort(str("revanced_debug_logs_disabled")); + return; + } + + // Show toast before clearing, otherwise toast log will still remain. + Utils.showToastShort(str("revanced_debug_logs_clear_toast")); + clearLogBufferData(); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java index 469319539..b1de29ee2 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java @@ -60,8 +60,9 @@ public class SortedListPreference extends ListPreference { } } + //noinspection ComparatorCombinators Collections.sort(lastEntries, (pair1, pair2) - -> pair1.first.compareToIgnoreCase(pair2.first)); + -> pair1.first.compareTo(pair2.first)); CharSequence[] sortedEntries = new CharSequence[entrySize]; CharSequence[] sortedEntryValues = new CharSequence[entrySize]; diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java index d2557e05c..d3e1baf7b 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java @@ -16,9 +16,7 @@ public class SpoofSimPatch { return false; } - Logger.initializationException(SpoofSimPatch.class, - "Context is not yet set, cannot spoof: " + fieldSpoofed, null); - + Logger.initializationException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null); return true; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index 8f7b46421..b7d1e2aee 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -317,11 +317,8 @@ public class Settings extends BaseSettings { public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE); public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true, "revanced_spoof_device_dimensions_user_dialog_message"); - /** - * When enabled, share the debug logs with care. - * The buffer contains select user data, including the client ip address and information that could identify the end user. - */ - public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, parent(BaseSettings.DEBUG)); + public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false, + "revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG)); // Swipe controls public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ClearLogBufferPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ClearLogBufferPreference.java new file mode 100644 index 000000000..109c6c8e7 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ClearLogBufferPreference.java @@ -0,0 +1,34 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; +import android.preference.Preference; +import app.revanced.extension.shared.settings.preference.LogBufferManager; + +/** + * A custom preference that clears the ReVanced debug log buffer when clicked. + * Invokes the {@link LogBufferManager#clearLogBuffer} method. + */ +@SuppressWarnings("unused") +public class ClearLogBufferPreference extends Preference { + + { + setOnPreferenceClickListener(pref -> { + LogBufferManager.clearLogBuffer(); + return true; + }); + } + + public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ClearLogBufferPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ClearLogBufferPreference(Context context) { + super(context); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExportLogToClipboardPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExportLogToClipboardPreference.java new file mode 100644 index 000000000..fac1cfa79 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExportLogToClipboardPreference.java @@ -0,0 +1,34 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; +import android.preference.Preference; +import app.revanced.extension.shared.settings.preference.LogBufferManager; + +/** + * A custom preference that triggers exporting ReVanced debug logs to the clipboard when clicked. + * Invokes the {@link LogBufferManager#exportToClipboard} method. + */ +@SuppressWarnings({"deprecation", "unused"}) +public class ExportLogToClipboardPreference extends Preference { + + { + setOnPreferenceClickListener(pref -> { + LogBufferManager.exportToClipboard(); + return true; + }); + } + + public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ExportLogToClipboardPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ExportLogToClipboardPreference(Context context) { + super(context); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java index 1cf2e0444..1b5e0aae5 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java @@ -376,7 +376,11 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { Utils.setEditTextDialogTheme(builder); builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { - Utils.setClipboard(getEditText().getText().toString()); + try { + Utils.setClipboard(getEditText().getText()); + } catch (Exception ex) { + Logger.printException(() -> "Copy settings failure", ex); + } }); } }; @@ -433,7 +437,11 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { Utils.setEditTextDialogTheme(builder); builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { - Utils.setClipboard(getEditText().getText().toString()); + try { + Utils.setClipboard(getEditText().getText()); + } catch (Exception ex) { + Logger.printException(() -> "Copy settings failure", ex); + } }); } }; diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt index f4facb097..d991b2d4b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt @@ -5,6 +5,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.revanced.patches.shared.misc.settings.preference.SwitchPreference @@ -23,7 +24,7 @@ private const val EXTENSION_CLASS_DESCRIPTOR = val enableDebuggingPatch = bytecodePatch( name = "Enable debugging", - description = "Adds options for debugging.", + description = "Adds options for debugging and exporting ReVanced logs to the clipboard.", ) { dependsOn( sharedExtensionPatch, @@ -56,6 +57,16 @@ val enableDebuggingPatch = bytecodePatch( SwitchPreference("revanced_debug_protobuffer"), SwitchPreference("revanced_debug_stacktrace"), SwitchPreference("revanced_debug_toast_on_error"), + NonInteractivePreference( + "revanced_debug_export_logs_to_clipboard", + tag = "app.revanced.extension.youtube.settings.preference.ExportLogToClipboardPreference", + selectable = true, + ), + NonInteractivePreference( + "revanced_debug_logs_clear_buffer", + tag = "app.revanced.extension.youtube.settings.preference.ClearLogBufferPreference", + selectable = true, + ), ), ), ) @@ -107,7 +118,6 @@ val enableDebuggingPatch = bytecodePatch( return-wide v0 """ ) - } experimentalStringFeatureFlagFingerprint.match( diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 86561c683..79a52e628 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -175,6 +175,11 @@ Tap the continue button and allow optimization changes." Log protocol buffer Debug logs include proto buffer Debug logs do not include proto buffer + "Enabling this setting will log additional layout data, including on-screen text for some UI components. + +This can help identify components when creating custom filters. + +However, enabling this will also log some user data such as your IP address." Log stack traces Debug logs include stack trace Debug logs do not include stack trace @@ -184,6 +189,15 @@ Tap the continue button and allow optimization changes." "Turning off error toasts hides all ReVanced error notifications. You will not be notified of any unexpected events." + Export debug logs + Copies ReVanced debug logs to the clipboard + Debug logging is disabled + No logs found + Logs copied + Failed to export logs: $s + Clear debug logs + Clears all stored ReVanced debug logs + Logs cleared Hide album cards