From b11b89561bc495e6faec340e00685fc6d5d80559 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 15 Oct 2026 20:40:24 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20Save=20report=20=E2=80=94=20stats?= =?UTF-8?q?=20and=20log=20in=20one=20file,=20from=20the=20app=20and=20the?= =?UTF-8?q?=20debug=20page=20(#207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DebugServer.reportText(): header (app, version, git hash, device, Android, time, uptime), /stats.json pretty-printed, the log ring; redacted again as a whole. The same text for both ways. - /report.txt: text/plain, Content-Disposition attachment with report-YYYYMMDD-HHMMSS.txt; LAN-only like the other paths. Save report button in the page header next to JSON and Log. - App: long press on Debug (touch, long OK on a remote, or Menu on Debug) saves Download/OpenIPC/report-....txt off the UI thread (MediaStore on Android 10+, storage permission on 6-9 with the app's own folder as a fallback), shows a toast and opens the Share sheet if an app can take it. - Docs: debug.md (endpoint, Save report, bug reports), usage.md, toys.md, security.md. Tests: report composition and redaction, /report.txt headers. - 2.11 (36). Fixes #207 --- android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 8 + .../java/com/openipc/decoder/DebugServer.java | 119 ++++++++++--- .../java/com/openipc/decoder/Decoder.java | 165 +++++++++++++++++- .../java/com/openipc/decoder/MenuManager.java | 20 +++ .../com/openipc/decoder/DebugServerTest.java | 73 ++++++++ docs/debug.md | 30 +++- docs/security.md | 3 +- docs/toys.md | 4 +- docs/usage.md | 2 +- 10 files changed, 396 insertions(+), 32 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index a725c471..d2f10b2d 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,8 +23,8 @@ android { applicationId "com.openipc.decoder" minSdk 21 targetSdk 36 - versionCode 35 - versionName "2.10" + versionCode 36 + versionName "2.11" buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\"" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 922a4fe1..bb26eb26 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -14,6 +14,14 @@ + + + + + + + + stats, String log, Date now, long uptimeS) { + StringBuilder sb = new StringBuilder(64 * 1024); + sb.append("OpenIPC Decoder report\n"); + sb.append("App: OpenIPC Decoder ").append(BuildConfig.VERSION_NAME) + .append(" (").append(BuildConfig.VERSION_CODE).append("), git ") + .append(BuildConfig.GIT_HASH).append('\n'); + sb.append("Device: ").append(android.os.Build.MANUFACTURER).append(' ') + .append(android.os.Build.MODEL).append('\n'); + sb.append("Android: ").append(android.os.Build.VERSION.RELEASE).append(" (SDK ") + .append(android.os.Build.VERSION.SDK_INT).append(")\n"); + sb.append("Time: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US) + .format(now)).append('\n'); + sb.append("Uptime: ").append(uptime(uptimeS)).append('\n'); + sb.append("\n== stats.json ==\n"); + sb.append(toJson(stats, true)); + sb.append("\n== log ==\n"); + sb.append(log != null ? log : ""); + return redact(sb.toString()); + } + + /** 1d 2h 3m 4s; the leading zero units left out. */ + static String uptime(long s) { + long d = s / 86400, h = s % 86400 / 3600, m = s % 3600 / 60; + StringBuilder sb = new StringBuilder(); + if (d > 0) sb.append(d).append("d "); + if (d > 0 || h > 0) sb.append(h).append("h "); + if (d > 0 || h > 0 || m > 0) sb.append(m).append("m "); + return sb.append(s % 60).append('s').toString(); + } + private Map snapshot() { Source s = source; Map m; @@ -270,11 +337,18 @@ private static void reply(OutputStream out, int code, String type, String body) private static void reply(OutputStream out, int code, String type, byte[] data) throws IOException { + reply(out, code, type, data, ""); + } + + /** {@code extra}: more header lines, each ending with \r\n. */ + private static void reply(OutputStream out, int code, String type, byte[] data, String extra) + throws IOException { String head = "HTTP/1.1 " + code + " " + reason(code) + "\r\n" + "Content-Type: " + type + "\r\n" + "Content-Length: " + data.length + "\r\n" + "Cache-Control: no-store\r\n" + (code == 405 ? "Allow: GET\r\n" : "") + + extra + "Connection: close\r\n\r\n"; out.write(head.getBytes(StandardCharsets.ISO_8859_1)); out.write(data); @@ -466,7 +540,8 @@ private static void closeQuietly(Socket s) { + "
" + "