From 18e4cffcb2060b5bdb863b6538b52c317a9dc101 Mon Sep 17 00:00:00 2001 From: mobile-kevin <306153527+mobile-kevin@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:57:10 +0200 Subject: [PATCH] fix(android): name a crash after its first frame outside the platform A Java crash with no 'Process:' line is named after the class of its first stack frame. The exception often surfaces in the framework, so a uiautomator crash was listed as 'android.os.Parcel'. Skip frames from platform packages (android., java., kotlin., dalvik. ...) and fall back to the first frame only when every frame belongs to the platform. --- devices/android_crash.go | 41 +++++++++++++++++++++++++++++-- devices/android_crash_test.go | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/devices/android_crash.go b/devices/android_crash.go index b684f022..ff6e31dd 100644 --- a/devices/android_crash.go +++ b/devices/android_crash.go @@ -187,17 +187,54 @@ func parseJavaCrash(lines []string, start int) (*CrashReport, int) { }, i } +// platformFramePrefixes are the packages of the runtime and libraries an app +// is built on. An exception often surfaces in one of them (android.os.Parcel, +// java.lang.Integer), which says nothing about which program crashed. +var platformFramePrefixes = []string{ + "android.", + "androidx.", + "com.android.internal.", + "dalvik.", + "java.", + "javax.", + "jdk.internal.", + "kotlin.", + "kotlinx.", + "libcore.", + "sun.", +} + +func isPlatformFrameOwner(owner string) bool { + for _, prefix := range platformFramePrefixes { + if strings.HasPrefix(owner, prefix) { + return true + } + } + return false +} + +// extractProcessFromStack names a crash that has no "Process:" line after the +// class of its first stack frame outside the platform, falling back to the +// first frame when every frame belongs to the platform. func extractProcessFromStack(lines []string, start int, pid string) string { + firstOwner := "" for i := start + 1; i < len(lines); i++ { parsed := parseLogcatLine(lines[i]) if parsed == nil || parsed.PID != pid || parsed.Tag != androidRuntimeTag { break } - if name := parseStackFrameOwner(parsed.Message); name != "" { + name := parseStackFrameOwner(parsed.Message) + if name == "" { + continue + } + if !isPlatformFrameOwner(name) { return name } + if firstOwner == "" { + firstOwner = name + } } - return "" + return firstOwner } // parseStackFrameOwner extracts the fully qualified class name from a stack diff --git a/devices/android_crash_test.go b/devices/android_crash_test.go index 43d970c9..320b9aed 100644 --- a/devices/android_crash_test.go +++ b/devices/android_crash_test.go @@ -165,6 +165,52 @@ func TestJavaCrashWithConstructorFrame(t *testing.T) { assert.Equal(t, "com.example.myapp.MyService", crashes[0].ProcessName) } +// a uiautomator crash taken from an emulator: the exception surfaces in the +// framework, and the crashing program's own code is further down the stack +const uiautomatorCrashLog = `2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: FATAL EXCEPTION: main +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: PID: 30490 +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: java.lang.IllegalStateException: UiAutomationService android.accessibilityservice.IAccessibilityServiceClient$Stub$Proxy@d09dec2already registered! +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: at android.os.Parcel.createExceptionOrNull(Parcel.java:3348) +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: at android.os.Parcel.createException(Parcel.java:3324) +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: at android.view.accessibility.IAccessibilityManager$Stub$Proxy.registerUiTestAutomationService(IAccessibilityManager.java:1154) +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: at android.app.UiAutomation.connect(UiAutomation.java:331) +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: at com.android.uiautomator.core.UiAutomationShellWrapper.connect(UiAutomationShellWrapper.java:36) +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: at com.android.commands.uiautomator.DumpCommand.run(DumpCommand.java:78) +2026-10-03 00:43:17.468 30490 30490 E AndroidRuntime: at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:371)` + +func TestJavaCrashWithoutProcessLineSkipsFrameworkFrames(t *testing.T) { + crashes := ParseAndroidCrashLog(uiautomatorCrashLog) + + require.Len(t, crashes, 1) + assert.Equal(t, "com.android.uiautomator.core.UiAutomationShellWrapper", crashes[0].ProcessName) +} + +func TestJavaCrashSkipsJavaAndKotlinFramesToFindTheAppFrame(t *testing.T) { + log := `2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: FATAL EXCEPTION: main +2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: java.lang.NumberFormatException: For input string: "x" +2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: at java.lang.Integer.parseInt(Integer.java:797) +2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: at kotlin.text.StringsKt__StringNumberConversionsJVMKt.toInt(StringNumberConversionsJVM.kt:10) +2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: at androidx.fragment.app.Fragment.performCreate(Fragment.java:3094) +2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: at com.example.myapp.SettingsFragment.onCreate(SettingsFragment.kt:21)` + + crashes := ParseAndroidCrashLog(log) + + require.Len(t, crashes, 1) + assert.Equal(t, "com.example.myapp.SettingsFragment", crashes[0].ProcessName) +} + +func TestJavaCrashWithOnlyFrameworkFramesFallsBackToTheFirstFrame(t *testing.T) { + log := `2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: FATAL EXCEPTION: main +2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: java.lang.RuntimeException +2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: at android.os.Looper.loop(Looper.java:288) +2026-03-27 14:00:00.000 12345 12345 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1004)` + + crashes := ParseAndroidCrashLog(log) + + require.Len(t, crashes, 1) + assert.Equal(t, "android.os.Looper", crashes[0].ProcessName) +} + func TestExtractedNativeCrashDoesNotIncludeJavaCrash(t *testing.T) { content, err := ExtractAndroidCrash(sampleCrashLog, "2026-03-02_10:58:32.108_1300")