diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ea73227b64..425473041a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,6 +12,8 @@ * Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for the currently selected tracks ([#2518](https://github.com/google/ExoPlayer/issues/2518)). + * Allow custom logger for all ExoPlayer log output + ([#9752](https://github.com/google/ExoPlayer/issues/9752)). * Extractors: * Add support for AVI ([#2092](https://github.com/google/ExoPlayer/issues/2092)). diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Log.java b/libraries/common/src/main/java/androidx/media3/common/util/Log.java index ce0d25dfad..b1a97f77fa 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Log.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Log.java @@ -18,6 +18,7 @@ package androidx.media3.common.util; import static java.lang.annotation.ElementType.TYPE_USE; import android.text.TextUtils; +import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.Size; @@ -28,7 +29,10 @@ import java.lang.annotation.Target; import java.net.UnknownHostException; import org.checkerframework.dataflow.qual.Pure; -/** Wrapper around {@link android.util.Log} which allows to set the log level. */ +/** + * Wrapper around {@link android.util.Log} which allows to set the log level and to specify a custom + * log output. + */ @UnstableApi public final class Log { @@ -52,15 +56,89 @@ public final class Log { /** Log level to disable all logging. */ public static final int LOG_LEVEL_OFF = Integer.MAX_VALUE; + /** + * Interface for a logger that can output messages with a tag. + * + *
Use {@link #DEFAULT} to output to {@link android.util.Log}. + */ + public interface Logger { + + /** The default instance logging to {@link android.util.Log}. */ + Logger DEFAULT = + new Logger() { + @Override + public void d(String tag, String message) { + android.util.Log.d(tag, message); + } + + @Override + public void i(String tag, String message) { + android.util.Log.i(tag, message); + } + + @Override + public void w(String tag, String message) { + android.util.Log.w(tag, message); + } + + @Override + public void e(String tag, String message) { + android.util.Log.e(tag, message); + } + }; + + /** + * Logs a debug-level message. + * + * @param tag The tag of the message. + * @param message The message. + */ + void d(String tag, String message); + + /** + * Logs an information-level message. + * + * @param tag The tag of the message. + * @param message The message. + */ + void i(String tag, String message); + + /** + * Logs a warning-level message. + * + * @param tag The tag of the message. + * @param message The message. + */ + void w(String tag, String message); + + /** + * Logs an error-level message. + * + * @param tag The tag of the message. + * @param message The message. + */ + void e(String tag, String message); + } + + private static final Object lock = new Object(); + + @GuardedBy("lock") private static int logLevel = LOG_LEVEL_ALL; + + @GuardedBy("lock") private static boolean logStackTraces = true; + @GuardedBy("lock") + private static Logger logger = Logger.DEFAULT; + private Log() {} /** Returns current {@link LogLevel} for ExoPlayer logcat logging. */ @Pure public static @LogLevel int getLogLevel() { - return logLevel; + synchronized (lock) { + return logLevel; + } } /** @@ -69,7 +147,9 @@ public final class Log { * @param logLevel The new {@link LogLevel}. */ public static void setLogLevel(@LogLevel int logLevel) { - Log.logLevel = logLevel; + synchronized (lock) { + Log.logLevel = logLevel; + } } /** @@ -79,7 +159,20 @@ public final class Log { * @param logStackTraces Whether stack traces will be logged. */ public static void setLogStackTraces(boolean logStackTraces) { - Log.logStackTraces = logStackTraces; + synchronized (lock) { + Log.logStackTraces = logStackTraces; + } + } + + /** + * Sets a custom {@link Logger} as the output. + * + * @param logger The {@link Logger}. + */ + public static void setLogger(Logger logger) { + synchronized (lock) { + Log.logger = logger; + } } /** @@ -87,8 +180,10 @@ public final class Log { */ @Pure public static void d(@Size(max = 23) String tag, String message) { - if (logLevel == LOG_LEVEL_ALL) { - android.util.Log.d(tag, message); + synchronized (lock) { + if (logLevel == LOG_LEVEL_ALL) { + logger.d(tag, message); + } } } @@ -105,8 +200,10 @@ public final class Log { */ @Pure public static void i(@Size(max = 23) String tag, String message) { - if (logLevel <= LOG_LEVEL_INFO) { - android.util.Log.i(tag, message); + synchronized (lock) { + if (logLevel <= LOG_LEVEL_INFO) { + logger.i(tag, message); + } } } @@ -123,8 +220,10 @@ public final class Log { */ @Pure public static void w(@Size(max = 23) String tag, String message) { - if (logLevel <= LOG_LEVEL_WARNING) { - android.util.Log.w(tag, message); + synchronized (lock) { + if (logLevel <= LOG_LEVEL_WARNING) { + logger.w(tag, message); + } } } @@ -141,8 +240,10 @@ public final class Log { */ @Pure public static void e(@Size(max = 23) String tag, String message) { - if (logLevel <= LOG_LEVEL_ERROR) { - android.util.Log.e(tag, message); + synchronized (lock) { + if (logLevel <= LOG_LEVEL_ERROR) { + logger.e(tag, message); + } } } @@ -168,20 +269,23 @@ public final class Log { @Nullable @Pure public static String getThrowableString(@Nullable Throwable throwable) { - if (throwable == null) { - return null; - } else if (isCausedByUnknownHostException(throwable)) { - // UnknownHostException implies the device doesn't have network connectivity. - // UnknownHostException.getMessage() may return a string that's more verbose than desired for - // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has - // special handling to return the empty string, which can result in logging that doesn't - // indicate the failure mode at all. Hence we special case this exception to always return a - // concise but useful message. - return "UnknownHostException (no network)"; - } else if (!logStackTraces) { - return throwable.getMessage(); - } else { - return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " "); + synchronized (lock) { + if (throwable == null) { + return null; + } else if (isCausedByUnknownHostException(throwable)) { + // UnknownHostException implies the device doesn't have network connectivity. + // UnknownHostException.getMessage() may return a string that's more verbose than desired + // for + // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has + // special handling to return the empty string, which can result in logging that doesn't + // indicate the failure mode at all. Hence we special case this exception to always return a + // concise but useful message. + return "UnknownHostException (no network)"; + } else if (!logStackTraces) { + return throwable.getMessage(); + } else { + return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " "); + } } }