Source added
This commit is contained in:
parent
b2864b500e
commit
ba28ca859e
8352 changed files with 1487182 additions and 1 deletions
17
debuglogs-viewer/app/build.gradle.kts
Normal file
17
debuglogs-viewer/app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
plugins {
|
||||
id("signal-sample-app")
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.signal.debuglogsviewer.app"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.signal.debuglogsviewer.app"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":debuglogs-viewer"))
|
||||
implementation(project(":core-util"))
|
||||
}
|
||||
26
debuglogs-viewer/app/src/main/AndroidManifest.xml
Normal file
26
debuglogs-viewer/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true">
|
||||
<activity android:name=".MainActivity"
|
||||
android:theme="@style/Theme.DebugLogs"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.wallet.api.enabled"
|
||||
android:value="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
59
debuglogs-viewer/app/src/main/assets/WebView.html
Normal file
59
debuglogs-viewer/app/src/main/assets/WebView.html
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<!--
|
||||
~ Copyright 2025 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
#container { position: absolute; top: 50px; bottom: 0; left: 0; right: 0; }
|
||||
#searchBar { position: fixed; top: 0; left: 0; width: 350px; display: none; padding: 10px; gap: 5px; z-index: 1; }
|
||||
#searchBar input { flex-grow: 1; padding: 5px ;}
|
||||
.searchMatches { position: absolute; background-color: rgba(182, 190, 250, 0.2); }
|
||||
#caseSensitiveButton.active { background-color: rgba(182, 190, 250, 0.2); }
|
||||
#matchCount { font-size: 10px; }
|
||||
#filterLevel { display: none; }
|
||||
|
||||
.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; }
|
||||
|
||||
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="searchBar">
|
||||
<button id="cancelButton"> x </button>
|
||||
<input type="text" id="searchInput" placeholder="Search" />
|
||||
<button id="prevButton"> < </button>
|
||||
<button id="nextButton"> > </button>
|
||||
<button id="caseSensitiveButton"> Cc </button>
|
||||
<div id="matchCount"> No match </div>
|
||||
</div>
|
||||
<div id="filterLevel">
|
||||
<div class="filterLevelMenu">
|
||||
<label><input type="checkbox" value=" V ">Verbose</label>
|
||||
<label><input type="checkbox" value=" D ">Debug</label>
|
||||
<label><input type="checkbox" value=" I ">Info</label>
|
||||
<label><input type="checkbox" value=" W ">Warning</label>
|
||||
<label><input type="checkbox" value=" E ">Error</label>
|
||||
<label><input type="checkbox" value="SignalUncaughtException">SignalUncaughtException</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="container">Loading...</div>
|
||||
<script src="https://www.unpkg.com/ace-builds@latest/src-noconflict/ace.js"></script>
|
||||
<script src="WebView.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
171
debuglogs-viewer/app/src/main/assets/WebView.js
Normal file
171
debuglogs-viewer/app/src/main/assets/WebView.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// 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;
|
||||
|
||||
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;
|
||||
|
||||
// Create Ace Editor using the custom mode
|
||||
let 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,
|
||||
});
|
||||
|
||||
// Get search bar functionalities
|
||||
const input = document.getElementById("searchInput");
|
||||
const prevButton = document.getElementById("prevButton");
|
||||
const nextButton = document.getElementById("nextButton");
|
||||
const cancelButton = document.getElementById("cancelButton");
|
||||
const caseSensitiveButton = document.getElementById("caseSensitiveButton");
|
||||
|
||||
// Generate highlight markers for all search matches
|
||||
const Range = ace.require("ace/range").Range;
|
||||
const session = editor.getSession();
|
||||
|
||||
let markers = []; // IDs of highlighted search markers
|
||||
let matchRanges = []; // Ranges of all search matches
|
||||
let matchCount = 0; // Total number of matches
|
||||
let caseSensitive = false;
|
||||
|
||||
// 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) {
|
||||
updateMatchPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
const searchTerm = caseSensitive ? term : term.toLowerCase();
|
||||
session
|
||||
.getDocument()
|
||||
.getAllLines()
|
||||
.forEach((line, row) => {
|
||||
let start = 0;
|
||||
const caseLine = caseSensitive ? 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;
|
||||
updateMatchPosition();
|
||||
}
|
||||
|
||||
input.addEventListener("input", () => highlightAllMatches(input.value));
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
// Update the display for current match
|
||||
function updateMatchPosition() {
|
||||
document.getElementById("matchCount").textContent = matchCount == 0 ? "No match" : `${getCurrentMatchIndex() + 1} / ${matchCount}`;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
prevButton.onclick = () => {
|
||||
editor.find(input.value, {
|
||||
backwards: true,
|
||||
wrap: true,
|
||||
skipCurrent: true,
|
||||
caseSensitive: caseSensitive,
|
||||
});
|
||||
updateMatchPosition();
|
||||
};
|
||||
|
||||
nextButton.onclick = () => {
|
||||
editor.find(input.value, {
|
||||
backwards: false,
|
||||
wrap: true,
|
||||
skipCurrent: true,
|
||||
caseSensitive: caseSensitive,
|
||||
});
|
||||
updateMatchPosition();
|
||||
};
|
||||
|
||||
cancelButton.onclick = () => {
|
||||
editor.getSelection().clearSelection();
|
||||
input.value = "";
|
||||
clearMarkers();
|
||||
updateMatchPosition();
|
||||
document.getElementById("searchBar").style.display = "none";
|
||||
};
|
||||
|
||||
caseSensitiveButton.onclick = () => {
|
||||
caseSensitive = !caseSensitive;
|
||||
highlightAllMatches(input.value);
|
||||
caseSensitiveButton.classList.toggle("active", caseSensitive);
|
||||
};
|
||||
|
||||
// Filter by log levels
|
||||
let logLines = "";
|
||||
function filterLogs() {
|
||||
const selectedLevels = Array.from(document.querySelectorAll('input[type="checkbox"]:checked')).map((cb) => cb.value);
|
||||
|
||||
if (selectedLevels.length === 0) {
|
||||
// If no level is selected, show all
|
||||
editor.setValue(logLines, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = logLines
|
||||
.split("\n")
|
||||
.filter((line) => {
|
||||
return selectedLevels.some((level) => line.includes(level));
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
editor.setValue(filtered, -1);
|
||||
}
|
||||
|
||||
document.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
|
||||
cb.addEventListener("change", filterLogs);
|
||||
});
|
||||
21
debuglogs-viewer/app/src/main/assets/log.txt
Normal file
21
debuglogs-viewer/app/src/main/assets/log.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
refrigerator euro-pop digital math- sign saturation point weathered tanto nano- disposable tanto pistol neon tube engine boy.
|
||||
computer rebar singularity boat San Francisco city spook shanty town cartel shoes car franchise tank-traps city nodality boy.
|
||||
tiger-team boy corrupted engine franchise tiger-team realism uplink realism sub-orbital motion systema voodoo god hacker urban Kowloon.
|
||||
rain knife cartel digital augmented reality office pre- beef noodles savant woman nano- garage numinous sunglasses augmented reality monofilament.
|
||||
|
||||
semiotics semiotics gang long-chain hydrocarbons post- tanto pen modem BASE jump sprawl marketing motion -ware convenience store
|
||||
crypto- chrome. denim fluidity woman sensory refrigerator drone realism long-chain hydrocarbons cyber- convenience store knife
|
||||
decay drone cartel katana carbon. pre- girl spook soul-delay chrome towards pistol singularity plastic decay market bicycle
|
||||
advert tank-traps geodesic grenade. RAF artisanal pre- savant neural order-flow pre- weathered Tokyo digital neural advert artisanal
|
||||
denim fluidity marketing.
|
||||
|
||||
Legba RAF assault RAF into rebar vinyl cardboard paranoid rifle euro-pop boy katana neon meta- neon. Legba papier-mache kanji faded
|
||||
bicycle A.I. sign nodal point semiotics modem urban DIY 8-bit cardboard sentient meta-. kanji assault rifle warehouse post- franchise
|
||||
skyscraper footage cardboard bomb drone drugs silent corrupted boat fetishism. towards rebar sentient face forwards meta- neon BASE
|
||||
jump papier-mache faded vinyl engine sunglasses claymore mine gang pen uplink.
|
||||
|
||||
Shibuya media BASE jump geodesic car sub-orbital j-pop semiotics boat rebar weathered claymore mine systemic skyscraper fluidity uplink.
|
||||
network pre- skyscraper market assassin footage tiger-team cyber- carbon claymore mine camera kanji shanty town A.I. katana long-chain
|
||||
hydrocarbons. savant stimulate physical katana 8-bit -ware paranoid systema neon market tiger-team San Francisco systemic engine numinous
|
||||
hacker. disposable weathered plastic warehouse papier-mache San Francisco range-rover woman katana San Francisco shanty town semiotics
|
||||
towards tower -ware denim.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.debuglogsviewer.app
|
||||
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.widget.Button
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import org.signal.debuglogsviewer.app.webview.setupWebView
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val webview: WebView = findViewById(R.id.webview)
|
||||
val findButton: Button = findViewById(R.id.findButton)
|
||||
val filterLevelButton: Button = findViewById(R.id.filterLevelButton)
|
||||
val editButton: Button = findViewById(R.id.editButton)
|
||||
val cancelEditButton: Button = findViewById(R.id.cancelEditButton)
|
||||
val copyButton: Button = findViewById(R.id.copyButton)
|
||||
|
||||
setupWebView(this, webview, findButton, filterLevelButton, editButton, cancelEditButton, copyButton)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.debuglogsviewer.app.webview
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Button
|
||||
import org.signal.debuglogsviewer.app.R
|
||||
|
||||
fun setupWebView(
|
||||
context: Context,
|
||||
webview: WebView,
|
||||
findButton: Button,
|
||||
filterLevelButton: Button,
|
||||
editButton: Button,
|
||||
cancelEditButton: Button,
|
||||
copyButton: Button
|
||||
) {
|
||||
val originalContent = org.json.JSONObject.quote(getLogText(context))
|
||||
var readOnly = true
|
||||
|
||||
webview.settings.apply {
|
||||
javaScriptEnabled = true
|
||||
builtInZoomControls = true
|
||||
displayZoomControls = false
|
||||
}
|
||||
|
||||
webview.loadUrl("file:///android_asset/debuglogs-viewer.html")
|
||||
|
||||
webview.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
// Set the debug log lines
|
||||
webview.evaluateJavascript("editor.setValue($originalContent, -1); logLines = $originalContent;", null)
|
||||
|
||||
// Set dark mode colors if in dark mode
|
||||
val isDarkMode = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
if (isDarkMode) {
|
||||
webview.evaluateJavascript("document.body.classList.add('dark');", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
findButton.setOnClickListener {
|
||||
webview.evaluateJavascript("document.getElementById('searchBar').style.display = 'block';", null)
|
||||
}
|
||||
|
||||
filterLevelButton.setOnClickListener {
|
||||
webview.evaluateJavascript("document.getElementById('filterLevel').style.display = 'block';", null)
|
||||
}
|
||||
|
||||
editButton.setOnClickListener {
|
||||
readOnly = !readOnly
|
||||
cancelEditButton.visibility = if (!readOnly) View.VISIBLE else View.GONE
|
||||
editButton.text = if (readOnly) "Enable Edit" else "Save Edit"
|
||||
webview.evaluateJavascript("editor.setReadOnly($readOnly);", null)
|
||||
}
|
||||
|
||||
cancelEditButton.setOnClickListener {
|
||||
readOnly = !readOnly
|
||||
cancelEditButton.visibility = View.GONE
|
||||
editButton.text = if (readOnly) "Enable Edit" else "Save Edit"
|
||||
webview.evaluateJavascript("editor.setReadOnly($readOnly);", null)
|
||||
webview.evaluateJavascript("editor.setValue($originalContent, -1);", null)
|
||||
}
|
||||
|
||||
copyButton.setOnClickListener { // In Signal app, use Util.writeTextToClipboard(context, value) instead
|
||||
webview.evaluateJavascript("editor.getValue();") { value ->
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(context.getString(R.string.app_name), value)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLogText(context: Context): String {
|
||||
return try {
|
||||
context.assets.open("log.txt").bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
"Error loading file: ${e.message}"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14.7,5.3A6.7,6.7 0,1 0,10 16.7a6.7,6.7 0,0 0,6.4 -5H14.7A5,5 0,1 1,10 5a4.9,4.9 0,0 1,3.5 1.5L10.8,9.2h5.9V3.3Z"/>
|
||||
</vector>
|
||||
64
debuglogs-viewer/app/src/main/res/layout/activity_main.xml
Normal file
64
debuglogs-viewer/app/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2025 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/buttonRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="top|end"
|
||||
android:padding="10dp"
|
||||
android:layout_marginTop="25dp"
|
||||
android:gravity="end">
|
||||
|
||||
<Button
|
||||
android:id="@+id/findButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:text="Find" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/filterLevelButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:text="Filter by Level" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/editButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:text="Enable Edit" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancelEditButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:text="Cancel Edit"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:text="Copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.DebugLogs" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
3
debuglogs-viewer/app/src/main/res/values/colors.xml
Normal file
3
debuglogs-viewer/app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
3
debuglogs-viewer/app/src/main/res/values/strings.xml
Normal file
3
debuglogs-viewer/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name" translatable="false">DebugLogs Viewer</string>
|
||||
</resources>
|
||||
8
debuglogs-viewer/app/src/main/res/values/themes.xml
Normal file
8
debuglogs-viewer/app/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.DebugLogs" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
28
debuglogs-viewer/lib/build.gradle.kts
Normal file
28
debuglogs-viewer/lib/build.gradle.kts
Normal 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)
|
||||
}
|
||||
5
debuglogs-viewer/lib/src/main/AndroidManifest.xml
Normal file
5
debuglogs-viewer/lib/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
9
debuglogs-viewer/lib/src/main/assets/ace.min.js
vendored
Normal file
9
debuglogs-viewer/lib/src/main/assets/ace.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
55
debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.html
Normal file
55
debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.html
Normal 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>
|
||||
244
debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.js
Normal file
244
debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.js
Normal 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();
|
||||
|
|
@ -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?
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue