From 373e19bd87d864719e34c36ab9dd327f977a9e5a Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sat, 9 May 2026 14:13:17 +0530 Subject: [PATCH] abd logs capture update --- .../gui/ui/components/SettingsDialog.kt | 172 ++++++++++++++++++ .../kotlin/app/morphe/gui/util/AdbManager.kt | 77 ++++++++ 2 files changed, 249 insertions(+) diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index 3124b946..de463055 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -40,8 +40,11 @@ import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.ui.theme.ThemePreference +import app.morphe.gui.util.AdbManager +import app.morphe.gui.util.DeviceMonitor import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger +import kotlinx.coroutines.launch import app.morphe.patcher.apk.ApkSigner import java.awt.Desktop import java.awt.FileDialog @@ -280,6 +283,18 @@ fun SettingsDialog( SettingsDivider(borderColor) + // ── Patched App Runtime Logs ── + PatchedAppRuntimeLogsSection( + mono = mono, + accentColor = accents.primary, + borderColor = borderColor, + enabled = !isPatching, + expanded = collapsibleSectionStates["RUNTIME LOGS"] == true, + onExpandedChange = { onCollapsibleSectionToggle("RUNTIME LOGS", it) } + ) + + SettingsDivider(borderColor) + // ── Patch Sources ── PatchSourcesSection( sources = patchSources, @@ -3118,3 +3133,160 @@ private fun resolveGitHubUrl(input: String): String? { return null } + +// ── Patched App Runtime Logs Section ── + +private sealed interface RuntimeLogsStatus { + data object Idle : RuntimeLogsStatus + data object Clearing : RuntimeLogsStatus + data object Saving : RuntimeLogsStatus + data object Cleared : RuntimeLogsStatus + data class Saved(val file: File, val lineCount: Int) : RuntimeLogsStatus + data class Error(val message: String) : RuntimeLogsStatus +} + +@Composable +private fun PatchedAppRuntimeLogsSection( + mono: androidx.compose.ui.text.font.FontFamily, + accentColor: Color, + borderColor: Color, + enabled: Boolean = true, + expanded: Boolean = false, + onExpandedChange: (Boolean) -> Unit = {} +) { + val monitorState by DeviceMonitor.state.collectAsState() + val selectedDevice = monitorState.selectedDevice + val scope = rememberCoroutineScope() + val adbManager = remember { AdbManager() } + var status by remember { mutableStateOf(RuntimeLogsStatus.Idle) } + + val isWorking = status is RuntimeLogsStatus.Clearing || status is RuntimeLogsStatus.Saving + val deviceReady = selectedDevice?.isReady == true + val canAct = enabled && deviceReady && !isWorking + + CollapsibleSection( + title = "PATCHED APP RUNTIME LOGS", + mono = mono, + expanded = expanded, + onExpandedChange = onExpandedChange + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = "Capture logs from your phone after a patched app crashes or misbehaves. Clear before reproducing the bug, then save the filtered output to attach to a bug report.", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + + // Device row + if (deviceReady) { + Text( + text = "Device: ${selectedDevice.displayName}${selectedDevice.architecture?.let { " ($it)" } ?: ""}", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } else { + Text( + text = "No device connected. Plug in your phone with USB debugging enabled.", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } + + ActionButton( + label = if (status is RuntimeLogsStatus.Clearing) "CLEARING…" else "CLEAR DEVICE LOGS", + icon = Icons.Default.DeleteSweep, + mono = mono, + borderColor = borderColor, + enabled = canAct, + onClick = { + val device = selectedDevice ?: return@ActionButton + status = RuntimeLogsStatus.Clearing + scope.launch { + val result = adbManager.clearLogcat(device.id) + status = result.fold( + onSuccess = { RuntimeLogsStatus.Cleared }, + onFailure = { RuntimeLogsStatus.Error(it.message ?: "Failed to clear logs") } + ) + } + } + ) + + ActionButton( + label = if (status is RuntimeLogsStatus.Saving) "SAVING…" else "SAVE DEVICE LOGS", + icon = Icons.Default.Save, + mono = mono, + borderColor = borderColor, + contentColor = accentColor, + enabled = canAct, + onClick = { + val device = selectedDevice ?: return@ActionButton + status = RuntimeLogsStatus.Saving + scope.launch { + val timestamp = SimpleDateFormat("yyyy-MM-dd-HHmmss", java.util.Locale.US).format(java.util.Date()) + val outFile = File(FileUtils.getLogsDir(), "device-logcat-$timestamp.txt") + val result = adbManager.captureLogcat(device.id, outFile) + status = result.fold( + onSuccess = { count -> RuntimeLogsStatus.Saved(outFile, count) }, + onFailure = { RuntimeLogsStatus.Error(it.message ?: "Failed to save logs") } + ) + } + } + ) + + // Status line + when (val s = status) { + RuntimeLogsStatus.Idle, RuntimeLogsStatus.Clearing, RuntimeLogsStatus.Saving -> Unit + RuntimeLogsStatus.Cleared -> Text( + text = "Logs cleared on device.", + fontSize = 11.sp, + fontFamily = mono, + color = accentColor.copy(alpha = 0.85f) + ) + is RuntimeLogsStatus.Saved -> Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = if (s.lineCount == 0) + "Nothing captured yet. Run the patched app on your phone, then save again." + else + "Saved ${s.lineCount} line(s) to ${s.file.name}", + fontSize = 11.sp, + fontFamily = mono, + color = if (s.lineCount == 0) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + else accentColor.copy(alpha = 0.85f) + ) + if (s.lineCount > 0) { + val cornersLocal = LocalMorpheCorners.current + Text( + text = "OPEN LOGS", + fontSize = 10.sp, + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.5.sp, + color = accentColor, + modifier = Modifier + .clip(RoundedCornerShape(cornersLocal.small)) + .clickable { + try { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(s.file.parentFile) + } + } catch (e: Exception) { + Logger.error("Failed to reveal logs folder", e) + } + } + .padding(horizontal = 10.dp, vertical = 6.dp) + ) + } + } + is RuntimeLogsStatus.Error -> Text( + text = s.message, + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.error + ) + } + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/util/AdbManager.kt b/src/main/kotlin/app/morphe/gui/util/AdbManager.kt index cfcdda20..d345989c 100644 --- a/src/main/kotlin/app/morphe/gui/util/AdbManager.kt +++ b/src/main/kotlin/app/morphe/gui/util/AdbManager.kt @@ -237,6 +237,83 @@ class AdbManager { } } + /** + * Clear the device's logcat buffers (main + crash). + * Crash buffer clear is best-effort — older devices may not have it. + */ + suspend fun clearLogcat(deviceId: String): Result = withContext(Dispatchers.IO) { + val adb = findAdb() ?: return@withContext Result.failure( + AdbException("ADB not found. Please install Android SDK Platform Tools.") + ) + + try { + val main = ProcessBuilder(adb, "-s", deviceId, "logcat", "-c") + .redirectErrorStream(true) + .start() + val mainOutput = main.inputStream.bufferedReader().readText() + if (main.waitFor() != 0) { + return@withContext Result.failure(AdbException("Failed to clear logs: $mainOutput")) + } + + // Best-effort: also clear the crash buffer. Ignore failure. + try { + val crash = ProcessBuilder(adb, "-s", deviceId, "logcat", "-b", "crash", "-c") + .redirectErrorStream(true) + .start() + crash.inputStream.bufferedReader().readText() + crash.waitFor() + } catch (_: Exception) { /* older devices may not have crash buffer */ } + + Logger.info("Cleared logcat on $deviceId") + Result.success(Unit) + } catch (e: Exception) { + Logger.error("Error clearing logcat", e) + Result.failure(AdbException("Failed to clear logs: ${e.message}")) + } + } + + /** + * Capture a logcat snapshot from the device, filtered to lines that contain + * "morphe:" or "AndroidRuntime", and write them to [outputFile]. + * Returns the number of lines written. + */ + suspend fun captureLogcat(deviceId: String, outputFile: File): Result = withContext(Dispatchers.IO) { + val adb = findAdb() ?: return@withContext Result.failure( + AdbException("ADB not found. Please install Android SDK Platform Tools.") + ) + + try { + val process = ProcessBuilder(adb, "-s", deviceId, "logcat", "-d", "-b", "main,crash") + .redirectErrorStream(true) + .start() + + val kept = mutableListOf() + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { line -> + if (line.contains("morphe:", ignoreCase = true) || line.contains("AndroidRuntime")) { + kept += line + } + } + } + val exitCode = process.waitFor() + if (exitCode != 0) { + return@withContext Result.failure(AdbException("logcat exited with code $exitCode")) + } + + if (kept.isEmpty()) { + Logger.info("No matching logcat lines on $deviceId — skipping file write") + } else { + outputFile.parentFile?.mkdirs() + outputFile.writeText(kept.joinToString("\n") + "\n") + Logger.info("Captured ${kept.size} logcat line(s) to ${outputFile.absolutePath}") + } + Result.success(kept.size) + } catch (e: Exception) { + Logger.error("Error capturing logcat", e) + Result.failure(AdbException("Failed to capture logs: ${e.message}")) + } + } + /** * Parse output from 'adb devices -l' command. * Example line: "XXXXXXXX device usb:1-1 product:flame model:Pixel_4 device:flame transport_id:1"