package com.fox2code.mmm.utils; import android.annotation.SuppressLint; import android.app.Activity; import android.util.Log; import android.widget.TextView; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import java.lang.ref.WeakReference; import java.util.LinkedList; import java.util.Objects; public class NoodleDebug { private static final String TAG = "NoodleDebug"; private static final WeakReference NULL_THREAD_REF = new WeakReference<>(null); private static final ThreadLocal THREAD_NOODLE = new ThreadLocal<>(); @SuppressLint("StaticFieldLeak") // <- Null initialized private static final NoodleDebug NULL = new NoodleDebug() { @Override public NoodleDebug bind() { getNoodleDebug().unbind(); THREAD_NOODLE.remove(); return this; } @Override public void setEnabled(boolean enabled) {} @Override protected void markDirty() {} }; private final Activity activity; private final TextView textView; private final LinkedList tokens; private final StringBuilder debug; private WeakReference thread; private boolean enabled, updating; private NoodleDebug() { this.activity = null; this.textView = null; this.tokens = new LinkedList<>(); this.debug = new StringBuilder(0); this.thread = NULL_THREAD_REF; } public NoodleDebug(Activity activity,@IdRes int textViewId) { this(activity, activity.findViewById(textViewId)); } public NoodleDebug(Activity activity, TextView textView) { this.activity = Objects.requireNonNull(activity); this.textView = Objects.requireNonNull(textView); this.tokens = new LinkedList<>(); this.debug = new StringBuilder(64); this.thread = NULL_THREAD_REF; } public NoodleDebug bind() { synchronized (this.tokens) { Thread thread; if ((thread = this.thread.get()) != null) { Log.e(TAG, "Trying to bind to thread \"" + Thread.currentThread().getName() + "\" while already bound to \"" + thread.getName() + "\""); return NULL; } this.tokens.clear(); } if (this.enabled) { this.thread = new WeakReference<>(Thread.currentThread()); THREAD_NOODLE.set(this); } else { this.thread = NULL_THREAD_REF; THREAD_NOODLE.remove(); } return this; } public void unbind() { this.thread = NULL_THREAD_REF; boolean markDirty; synchronized (this.tokens) { markDirty = !this.tokens.isEmpty(); this.tokens.clear(); } if (markDirty) this.markDirty(); } public boolean isBound() { return this.thread.get() != null; } public void push(String token) { if (!this.enabled) return; synchronized (this.tokens) { this.tokens.add(token); } if (!token.isEmpty()) this.markDirty(); } public void pop() { if (!this.enabled) return; String last; synchronized (this.tokens) { last = this.tokens.removeLast(); } if (!last.isEmpty()) this.markDirty(); } public void replace(String token) { if (!this.enabled) return; String last; synchronized (this.tokens) { last = this.tokens.removeLast(); this.tokens.add(token); } if (!last.equals(token)) this.markDirty(); } public void setEnabled(boolean enabled) { if (this.enabled && !enabled) { this.thread = NULL_THREAD_REF; synchronized (this.tokens) { this.tokens.clear(); } this.markDirty(); } this.enabled = enabled; } protected void markDirty() { assert this.activity != null; assert this.textView != null; if (this.updating) return; this.updating = true; this.activity.runOnUiThread(() -> { String debugText; synchronized (this.tokens) { StringBuilder debug = this.debug; debug.setLength(0); boolean first = true; for (String text : this.tokens) { if (text.isEmpty()) continue; if (first) first = false; else debug.append(" > "); debug.append(text); } debugText = debug.toString(); } this.updating = false; this.textView.setText(debugText); }); } @NonNull public static NoodleDebug getNoodleDebug() { NoodleDebug noodleDebug = THREAD_NOODLE.get(); if (noodleDebug == null) return NULL; if (noodleDebug.thread.get() != Thread.currentThread() || noodleDebug.activity.isDestroyed()) { THREAD_NOODLE.remove(); return NULL; } return noodleDebug; } }