Source added

This commit is contained in:
Fr4nz D13trich 2025-11-20 09:26:33 +01:00
parent b2864b500e
commit ba28ca859e
8352 changed files with 1487182 additions and 1 deletions

View file

@ -0,0 +1,28 @@
plugins {
id("signal-library")
id("kotlin-parcelize")
}
android {
namespace = "org.signal.debuglogsviewer"
buildFeatures {
buildConfig = true
}
}
dependencies {
implementation(project(":core-util"))
implementation(project(":core-util-jvm"))
implementation(libs.kotlin.reflect)
implementation(libs.jackson.module.kotlin)
implementation(libs.jackson.core)
testImplementation(testLibs.robolectric.robolectric) {
exclude(group = "com.google.protobuf", module = "protobuf-java")
}
api(libs.google.play.services.wallet)
api(libs.square.okhttp3)
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,55 @@
<!--
~ Copyright 2025 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* We keep the background transparent to avoid white flashes in dark theme */
html, body {
background: transparent;
margin: 0;
padding: 0;
}
#container { position: absolute; top: 0; bottom: 0; left: 0; right: 0; height: 100%; width: 100%; }
.searchMatches { position: absolute; background-color: rgba(182, 190, 250, 0.4); }
/* Scrollbar Setup */
.ace_scrollbar::-webkit-scrollbar { width: 0; height: 0; }
.show-scrollbar .ace_scrollbar::-webkit-scrollbar { width: 5px; height: 5px; }
.ace_scrollbar::-webkit-scrollbar-thumb { background-color: #999999; }
/* Hide mobile context menu */
.ace_mobile-menu {
display: none !important;
}
/* Light Mode: Line color based on log level */
.ace_editor { background-color: #FBFCFF; }
.ace_none { color: #000000; }
.ace_verbose { color: #515151; }
.ace_debug { color: #089314; }
.ace_info { color: #0a7087; }
.ace_warning { color: #b58c12; }
.ace_error { color: #af0d0a; }
/* Dark Mode: Line color based on log level */
body.dark .ace_editor { background-color: #1B1C1F; }
body.dark .ace_none { color: #ffffff; }
body.dark .ace_verbose { color: #8a8a8a; }
body.dark .ace_debug { color: #5ca72b; }
body.dark .ace_info { color: #46bbb9; }
body.dark .ace_warning { color: #cdd637; }
body.dark .ace_error { color: #ff6b68; }
</style>
</head>
<body>
<div id="container"></div>
<script src="ace.min.js"></script>
<script src="debuglogs-viewer.js"></script>
</body>
</html>

View file

@ -0,0 +1,244 @@
let editor;
let timeout;
let session;
let logLines = ""; // Original logLines
let input = ""; // Search query input
let selectedLevels = []; // Log levels that are selected in checkboxes
let markers = []; // IDs of highlighted search markers
let matchRanges = []; // Ranges of all search matches
let matchCount = 0; // Total number of matches
let isFiltered = false;
let isCaseSensitive = false;
// Create custom text mode to color different levels of debug log lines
const TextMode = ace.require("ace/mode/text").Mode;
const TextHighlightRules = ace.require("ace/mode/text_highlight_rules").TextHighlightRules;
const Range = ace.require("ace/range").Range;
function main() {
// Create Ace Editor using the custom mode
editor = ace.edit("container", {
mode: new CustomMode(),
theme: "ace/theme/textmate",
wrap: false, // Allow for horizontal scrolling
readOnly: true,
showGutter: false,
highlightActiveLine: false,
highlightSelectedWord: false, // Prevent Ace Editor from automatically highlighting all instances of a selected word (really laggy!)
showPrintMargin: false,
});
editor.session.on("changeScrollTop", showScrollBar);
editor.session.on("changeScrollLeft", showScrollBar);
// Generate highlight markers for all search matches
session = editor.getSession();
}
function CustomHighlightRules() {
this.$rules = {
start: [
{ token: "verbose", regex: "^.*\\sV\\s.*$" },
{ token: "debug", regex: "^.*\\sD\\s.*$" },
{ token: "info", regex: "^.*\\sI\\s.*$" },
{ token: "warning", regex: "^.*\\sW\\s.*$" },
{ token: "error", regex: "^.*\\sE\\s.*$" },
{ token: "none", regex: ".*" },
],
};
}
CustomHighlightRules.prototype = new TextHighlightRules();
function CustomMode() {
TextMode.call(this);
this.HighlightRules = CustomHighlightRules;
}
CustomMode.prototype = Object.create(TextMode.prototype);
CustomMode.prototype.constructor = CustomMode;
// Show scrollbar that fades after a second since last scroll
function showScrollBar() {
editor.container.classList.add("show-scrollbar");
clearTimeout(timeout);
timeout = setTimeout(() => editor.container.classList.remove("show-scrollbar"), 1000);
}
// Clear all search markers and match info
function clearMarkers() {
markers.forEach((id) => session.removeMarker(id));
markers = [];
matchRanges = [];
matchCount = 0;
}
// Highlight all instances of the search term
function highlightAllMatches(term) {
clearMarkers();
if (!term) {
return;
}
const searchTerm = isCaseSensitive ? term : term.toLowerCase();
session
.getDocument()
.getAllLines()
.forEach((line, row) => {
let start = 0;
const caseLine = isCaseSensitive ? line : line.toLowerCase();
while (true) {
const index = caseLine.indexOf(searchTerm, start);
if (index === -1) {
break;
}
const range = new Range(row, index, row, index + term.length);
markers.push(session.addMarker(range, "searchMatches", "text", false));
matchRanges.push(range);
start = index + term.length;
}
});
matchCount = markers.length;
}
// Return index of current match
function getCurrentMatchIndex() {
const current = editor.getSelection().getRange();
return matchRanges.findIndex(
(r) =>
r.start.row === current.start.row &&
r.start.column === current.start.column &&
r.end.row === current.end.row &&
r.end.column === current.end.column,
);
}
function getSearchPosition() {
if (input == "") {
return "";
}
return matchCount == 0 ? "No match" : `${getCurrentMatchIndex() + 1} of ${matchCount}`;
}
function onSearchUp() {
editor.find(input, {
backwards: true,
wrap: true,
skipCurrent: true,
caseSensitive: isCaseSensitive,
});
}
function onSearchDown() {
editor.find(input, {
backwards: false,
wrap: true,
skipCurrent: true,
caseSensitive: isCaseSensitive,
});
}
function onSearchClose() {
editor.setValue(logLines, -1);
editor.getSelection().clearSelection();
input = "";
clearMarkers();
}
function onToggleCaseSensitive() {
isCaseSensitive = !isCaseSensitive;
(isFiltered) ? onFilter() : highlightAllMatches(input);
}
function onSearchInput(value) {
input = value;
highlightAllMatches(input);
editor.find(input, {
backwards: false,
wrap: true,
skipCurrent: false,
caseSensitive: isCaseSensitive,
});
}
function onSearch() {
highlightAllMatches(input);
}
function onFilter() {
isFiltered = true;
editor.getSelection().clearSelection();
clearMarkers();
applyFilter();
}
function onFilterClose() {
if (isFiltered) {
isFiltered = false;
if (selectedLevels.length === 0) {
editor.setValue(logLines, -1);
} else {
const filtered = logLines
.split("\n")
.filter((line) => {
return selectedLevels.some((level) => line.includes(level));
})
.join("\n");
editor.setValue(filtered, -1);
}
highlightAllMatches(input);
}
}
function onFilterLevel(sLevels) {
selectedLevels = sLevels;
if (isFiltered) {
applyFilter();
} else {
if (selectedLevels.length === 0) {
editor.setValue(logLines, -1);
editor.scrollToRow(0);
} else {
const filtered = logLines
.split("\n")
.filter((line) => {
return selectedLevels.some((level) => line.includes(level));
})
.join("\n");
editor.setValue(filtered, -1);
}
onSearch();
}
}
function applyFilter() {
const filtered = logLines
.split("\n")
.filter((line) => {
const newLine = isCaseSensitive ? line : line.toLowerCase();
const lineMatch = newLine.includes(isCaseSensitive ? input : input.toLowerCase());
const levelMatch = selectedLevels.length === 0 || selectedLevels.some((level) => line.includes(level));
return lineMatch && levelMatch;
})
.join("\n");
editor.setValue(filtered, -1);
}
function appendLines(lines) {
editor.session.insert({ row: editor.session.getLength(), column: 0}, lines);
logLines += lines;
}
function readLines(offset, limit) {
const lines = logLines.split("\n")
if (offset >= lines.length) {
return "<<END OF INPUT>>";
}
return lines.slice(offset, offset + limit).join("\n")
}
main();

View file

@ -0,0 +1,148 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.debuglogsviewer
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.webkit.ValueCallback
import android.webkit.WebView
import android.webkit.WebViewClient
import kotlinx.coroutines.Runnable
import org.json.JSONArray
import org.json.JSONObject
import org.signal.core.util.ThreadUtil
import java.util.concurrent.CountDownLatch
import java.util.function.Consumer
object DebugLogsViewer {
@JvmStatic
fun initWebView(webview: WebView, context: Context, onFinished: Runnable) {
webview.settings.apply {
javaScriptEnabled = true
builtInZoomControls = true
displayZoomControls = false
}
webview.isVerticalScrollBarEnabled = false
webview.isHorizontalScrollBarEnabled = false
webview.setBackgroundColor(Color.TRANSPARENT)
webview.background = null
webview.loadUrl("file:///android_asset/debuglogs-viewer.html")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
if (context.isDarkTheme) {
webview.evaluateJavascript("document.body.classList.add('dark');", null)
}
onFinished.run()
}
}
}
@JvmStatic
fun appendLines(webview: WebView, lines: String) {
// Set the debug log lines
val escaped = JSONObject.quote(lines)
val latch = CountDownLatch(1)
ThreadUtil.runOnMain {
webview.evaluateJavascript("appendLines($escaped)") { latch.countDown() }
}
latch.await()
}
@JvmStatic
fun readLogs(webview: WebView): LogReader {
var position = 0
return LogReader { size ->
val latch = CountDownLatch(1)
var result: String? = null
ThreadUtil.runOnMain {
webview.evaluateJavascript("readLines($position, $size)") { value ->
// Annoying, string returns from javascript land are always encoded as JSON strings (wrapped in quotes, various strings escaped, etc)
val parsed = JSONArray("[$value]").getString(0)
result = parsed.takeUnless { it == "<<END OF INPUT>>" }
position += size
latch.countDown()
}
}
latch.await()
result
}
}
@JvmStatic
fun scrollToTop(webview: WebView) {
webview.evaluateJavascript("editor.scrollToRow(0);", null)
}
@JvmStatic
fun scrollToBottom(webview: WebView) {
webview.evaluateJavascript("editor.scrollToRow(editor.session.getLength() - 1);", null)
}
@JvmStatic
fun onSearchInput(webview: WebView, query: String) {
val escaped = JSONObject.quote(query)
webview.evaluateJavascript("onSearchInput($escaped)", null)
}
@JvmStatic
fun onSearch(webview: WebView) {
webview.evaluateJavascript("onSearch()", null)
}
@JvmStatic
fun onFilter(webview: WebView) {
webview.evaluateJavascript("onFilter()", null)
}
@JvmStatic
fun onFilterClose(webview: WebView) {
webview.evaluateJavascript("onFilterClose()", null)
}
@JvmStatic
fun onFilterLevel(webview: WebView, selectedLevels: String) {
webview.evaluateJavascript("if (isFiltered) { onFilter(); }", null)
webview.evaluateJavascript("onFilterLevel($selectedLevels)", null)
}
@JvmStatic
fun onSearchUp(webview: WebView) {
webview.evaluateJavascript("onSearchUp();", null)
}
@JvmStatic
fun onSearchDown(webview: WebView) {
webview.evaluateJavascript("onSearchDown();", null)
}
@JvmStatic
fun getSearchPosition(webView: WebView, callback: Consumer<String?>) {
webView.evaluateJavascript("getSearchPosition();", ValueCallback { value: String? -> callback.accept(value?.trim('"') ?: "") })
}
@JvmStatic
fun onToggleCaseSensitive(webview: WebView) {
webview.evaluateJavascript("onToggleCaseSensitive();", null)
}
@JvmStatic
fun onSearchClose(webview: WebView) {
webview.evaluateJavascript("onSearchClose();", null)
}
private val Context.isDarkTheme
get() = (this.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
fun interface LogReader {
/** Returns the next bit of log, containing at most [size] lines (but may be less), or null if there are no logs remaining. */
fun nextChunk(size: Int): String?
}
}