Skip to content

Instantly share code, notes, and snippets.

@yesitskev
Last active July 13, 2018 16:06
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save yesitskev/e93c09c6b5af3339a3cb to your computer and use it in GitHub Desktop.
Save yesitskev/e93c09c6b5af3339a3cb to your computer and use it in GitHub Desktop.
Stetho Timber Logger
import android.util.Log;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer;
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod;
import org.json.JSONObject;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import timber.log.Timber;
/**
* Log things to console (hacked job).
*
* This probably isn't too useful right now as Stetho is still very immature. The class name
* has to be called console or some other chrome handler name. For some reason Stetho is using
* the class name to bind to the corresponding chrome handle.
*
* Ideally, in the future Stetho should have a class annotation marking the handle to avoid this
* 1 to 1 relationship of handle binding.
*/
public static class Console extends com.facebook.stetho.inspector.protocol.module.Console implements Timber.TaggedTree {
private static final int MAX_LOG_LENGTH = 4000;
private static final Pattern ANONYMOUS_CLASS = Pattern.compile("\\$\\d+$");
private static final ThreadLocal<String> NEXT_TAG = new ThreadLocal<>();
JsonRpcPeer peer;
public Console() {
// Empty.
}
@Override
@ChromeDevtoolsMethod
public void enable(JsonRpcPeer peer, JSONObject params) {
this.peer = peer;
}
@Override
@ChromeDevtoolsMethod
public void disable(JsonRpcPeer peer, JSONObject params) {
this.peer = null;
}
private static String maybeFormat(String message, Object... args) {
// If no varargs are supplied, treat it as a request to log the string without formatting.
return args.length == 0 ? message : String.format(message, args);
}
@Override public final void tag(String tag) {
NEXT_TAG.set(tag);
}
/**
* Returns an explicitly set tag for the next log message or {@code null}. Calling this method
* clears any set tag so it may only be called once.
*/
protected final String nextTag() {
String tag = NEXT_TAG.get();
if (tag != null) {
NEXT_TAG.remove();
}
return tag;
}
/**
* Creates a tag for a log message.
* <p>
* By default this method will check {@link #nextTag()} for an explicit tag. If there is no
* explicit tag, the class name of the caller will be used by inspecting the stack trace of the
* current thread.
* <p>
* Note: Do not call {@code super.createTag()} if you override this method. It will produce
* incorrect results.
*/
protected String createTag() {
String tag = nextTag();
if (tag != null) {
return tag;
}
// DO NOT switch this to Thread.getCurrentThread().getStackTrace(). The test will pass
// because Robolectric runs them on the JVM but on Android the elements are different.
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
if (stackTrace.length < 6) {
throw new IllegalStateException(
"Synthetic stacktrace didn't have enough elements: are you using proguard?");
}
tag = stackTrace[5].getClassName();
Matcher m = ANONYMOUS_CLASS.matcher(tag);
if (m.find()) {
tag = m.replaceAll("");
}
return tag.substring(tag.lastIndexOf('.') + 1);
}
private void throwShade(MessageLevel priority, String message, Throwable t) {
if (message == null || message.length() == 0) {
if (t == null) {
return; // Swallow message if it's null and there's no throwable.
}
message = Log.getStackTraceString(t);
} else if (t != null) {
message += "\n" + Log.getStackTraceString(t);
}
String tag = createTag();
logMessage(priority, tag, message);
}
/** Log a message! */
protected void logMessage(MessageLevel priority, String tag, String message) {
if (peer == null) return;
Console.MessageAddedRequest request = new Console.MessageAddedRequest();
Console.ConsoleMessage console = new Console.ConsoleMessage();
request.message = console;
console.source = MessageSource.CONSOLE_API;
console.level = priority;
if (message.length() < MAX_LOG_LENGTH) {
console.text = String.format("[%s] %s", tag, message);
peer.invokeMethod("Console.messageAdded", request, null);
return;
}
// Split by line, then ensure each line can fit into Log's maximum length.
for (int i = 0, length = message.length(); i < length; i++) {
int newline = message.indexOf('\n', i);
newline = newline != -1 ? newline : length;
do {
int end = Math.min(newline, i + MAX_LOG_LENGTH);
console.text = String.format("[%s] %s", tag, message.substring(i, end));
peer.invokeMethod("Console.messageAdded", request, null);
i = end;
} while (i < newline);
}
}
@Override public final void v(String message, Object... args) {
throwShade(MessageLevel.LOG, maybeFormat(message, args), null);
}
@Override public final void v(Throwable t, String message, Object... args) {
throwShade(MessageLevel.LOG, maybeFormat(message, args), t);
}
@Override public final void d(String message, Object... args) {
throwShade(MessageLevel.DEBUG, maybeFormat(message, args), null);
}
@Override public final void d(Throwable t, String message, Object... args) {
throwShade(MessageLevel.DEBUG, maybeFormat(message, args), t);
}
@Override public final void i(String message, Object... args) {
throwShade(MessageLevel.LOG, maybeFormat(message, args), null);
}
@Override public final void i(Throwable t, String message, Object... args) {
throwShade(MessageLevel.LOG, maybeFormat(message, args), t);
}
@Override public final void w(String message, Object... args) {
throwShade(MessageLevel.WARNING, maybeFormat(message, args), null);
}
@Override public final void w(Throwable t, String message, Object... args) {
throwShade(MessageLevel.WARNING, maybeFormat(message, args), t);
}
@Override public final void e(String message, Object... args) {
throwShade(MessageLevel.ERROR, maybeFormat(message, args), null);
}
@Override public final void e(Throwable t, String message, Object... args) {
throwShade(MessageLevel.ERROR, maybeFormat(message, args), t);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment