Skip to content

Commit b0bfe0d

Browse files
authored
[Android] [gradle-test-app] Revamp app terminations (#929)
1 parent a6120e0 commit b0bfe0d

File tree

8 files changed

+277
-24
lines changed

8 files changed

+277
-24
lines changed

platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/data/model/Actions.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ sealed class DiagnosticsAction : AppAction {
4545

4646
object ForceAppExit : DiagnosticsAction()
4747

48+
object TriggerRandomNativeCrash : DiagnosticsAction()
49+
50+
object TriggerRandomJvmCrash : DiagnosticsAction()
51+
52+
object TriggerRandomAnrCrash : DiagnosticsAction()
53+
4854
data class UpdateAppExitReason(
4955
val reason: AppExitReason,
5056
) : DiagnosticsAction()

platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ui/compose/MainScreen.kt

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.*
1212
import androidx.compose.foundation.lazy.LazyColumn
1313
import androidx.compose.foundation.lazy.rememberLazyListState
1414
import androidx.compose.material.icons.Icons
15+
import androidx.compose.material.icons.automirrored.filled.ExitToApp
1516
import androidx.compose.material.icons.filled.Home
1617
import androidx.compose.material.icons.filled.Menu
1718
import androidx.compose.material.icons.filled.PlayArrow
@@ -46,6 +47,8 @@ import io.bitdrift.gradletestapp.data.model.FeatureFlagsTestAction
4647
import io.bitdrift.gradletestapp.data.model.GlobalFieldAction
4748
import io.bitdrift.gradletestapp.data.model.NetworkTestAction
4849
import io.bitdrift.gradletestapp.data.model.SessionAction
50+
import io.bitdrift.gradletestapp.ui.compose.components.AppTerminationsCard
51+
import io.bitdrift.gradletestapp.ui.compose.components.FatalIssuesCard
4952
import io.bitdrift.gradletestapp.ui.compose.components.GlobalFieldsCard
5053
import io.bitdrift.gradletestapp.ui.compose.components.FeatureFlagsTestingCard
5154
import io.bitdrift.gradletestapp.ui.compose.components.NavigationCard
@@ -64,6 +67,7 @@ private enum class BottomNavTab(
6467
) {
6568
HOME("Home", "home_tab", Icons.Default.Home),
6669
SDK_APIS("SDK APIs", "sdk_apis_tab", Icons.Default.PlayArrow),
70+
APP_TERMINATIONS("App Exits", "app_exits_tab", Icons.AutoMirrored.Filled.ExitToApp),
6771
STRESS_TESTS("Stress", "stress_tests_tab", Icons.Default.Warning),
6872
NAVIGATE("Navigate", "navigate_tab", Icons.Default.Menu),
6973
SETTINGS("Settings", "settings_tab", Icons.Default.Settings),
@@ -167,6 +171,10 @@ fun MainScreen(
167171
BottomNavTab.STRESS_TESTS -> StressTestsTabContent(
168172
onAction = onAction,
169173
)
174+
BottomNavTab.APP_TERMINATIONS -> AppTerminationsTabContent(
175+
uiState = uiState,
176+
onAction = onAction,
177+
)
170178
BottomNavTab.NAVIGATE -> NavigateTabContent(
171179
onAction = onAction,
172180
)
@@ -293,12 +301,10 @@ private fun SdkApisTabContent(
293301
TestingToolsCard(
294302
uiState = uiState,
295303
onLogLevelChange = { onAction(ConfigAction.UpdateLogLevel(it)) },
296-
onAppExitReasonChange = { onAction(DiagnosticsAction.UpdateAppExitReason(it)) },
297304
onLogMessage = {
298305
onAction(DiagnosticsAction.LogMessage)
299306
Toast.makeText(context, toasterText, Toast.LENGTH_SHORT).show()
300307
},
301-
onAction = onAction,
302308
)
303309
}
304310
item {
@@ -337,6 +343,37 @@ private fun SdkApisTabContent(
337343
}
338344
}
339345

346+
@Composable
347+
private fun AppTerminationsTabContent(
348+
uiState: AppState,
349+
onAction: (AppAction) -> Unit,
350+
) {
351+
val listState = rememberLazyListState()
352+
353+
LazyColumn(
354+
state = listState,
355+
modifier =
356+
Modifier
357+
.fillMaxSize()
358+
.padding(horizontal = 16.dp),
359+
verticalArrangement = Arrangement.spacedBy(12.dp),
360+
contentPadding = PaddingValues(vertical = 8.dp),
361+
) {
362+
item {
363+
AppTerminationsCard(
364+
uiState = uiState,
365+
onAppExitReasonChange = { onAction(DiagnosticsAction.UpdateAppExitReason(it)) },
366+
onAction = onAction,
367+
)
368+
}
369+
item {
370+
FatalIssuesCard(
371+
onAction = onAction,
372+
)
373+
}
374+
}
375+
}
376+
340377
@Composable
341378
private fun StressTestsTabContent(
342379
onAction: (AppAction) -> Unit,
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
package io.bitdrift.gradletestapp.ui.compose.components
9+
10+
import androidx.compose.foundation.BorderStroke
11+
import androidx.compose.foundation.layout.Arrangement
12+
import androidx.compose.foundation.layout.Column
13+
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.padding
15+
import androidx.compose.material3.Button
16+
import androidx.compose.material3.ButtonDefaults
17+
import androidx.compose.material3.Card
18+
import androidx.compose.material3.CardDefaults
19+
import androidx.compose.material3.MaterialTheme
20+
import androidx.compose.material3.Text
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.graphics.Color
24+
import androidx.compose.ui.unit.dp
25+
import io.bitdrift.gradletestapp.data.model.AppAction
26+
import io.bitdrift.gradletestapp.data.model.AppState
27+
import io.bitdrift.gradletestapp.data.model.DiagnosticsAction
28+
import io.bitdrift.gradletestapp.ui.theme.BitdriftColors
29+
30+
@Composable
31+
fun AppTerminationsCard(
32+
uiState: AppState,
33+
onAppExitReasonChange: (io.bitdrift.gradletestapp.data.model.AppExitReason) -> Unit,
34+
onAction: (AppAction) -> Unit,
35+
modifier: Modifier = Modifier,
36+
) {
37+
Card(
38+
modifier = modifier.fillMaxWidth(),
39+
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
40+
colors =
41+
CardDefaults.cardColors(
42+
containerColor = BitdriftColors.BackgroundPaper,
43+
),
44+
shape = MaterialTheme.shapes.medium,
45+
border =
46+
BorderStroke(
47+
width = 1.dp,
48+
color = BitdriftColors.Border.copy(alpha = 0.3f),
49+
),
50+
) {
51+
Column(
52+
modifier = Modifier.padding(16.dp),
53+
verticalArrangement = Arrangement.spacedBy(16.dp),
54+
) {
55+
Text(
56+
text = "App Terminations",
57+
style = MaterialTheme.typography.titleMedium,
58+
color = BitdriftColors.TextPrimary,
59+
)
60+
61+
AppExitReasonSelector(
62+
selectedAppExitReason = uiState.diagnostics.selectedAppExitReason,
63+
onAppExitReasonChange = onAppExitReasonChange,
64+
)
65+
66+
TerminationButton(
67+
text = "Force App Exit",
68+
onClick = { onAction(DiagnosticsAction.ForceAppExit) },
69+
containerColor = BitdriftColors.Error,
70+
)
71+
72+
Text(
73+
text = "Choose a specific termination reason for force-exit testing.",
74+
style = MaterialTheme.typography.bodySmall,
75+
color = BitdriftColors.TextSecondary,
76+
)
77+
}
78+
}
79+
}
80+
81+
@Composable
82+
fun FatalIssuesCard(
83+
onAction: (AppAction) -> Unit,
84+
modifier: Modifier = Modifier,
85+
) {
86+
Card(
87+
modifier = modifier.fillMaxWidth(),
88+
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
89+
colors =
90+
CardDefaults.cardColors(
91+
containerColor = BitdriftColors.BackgroundPaper,
92+
),
93+
shape = MaterialTheme.shapes.medium,
94+
border =
95+
BorderStroke(
96+
width = 1.dp,
97+
color = BitdriftColors.Border.copy(alpha = 0.3f),
98+
),
99+
) {
100+
Column(
101+
modifier = Modifier.padding(16.dp),
102+
verticalArrangement = Arrangement.spacedBy(16.dp),
103+
) {
104+
Text(
105+
text = "Trigger a Random Fatal Issue",
106+
style = MaterialTheme.typography.titleMedium,
107+
color = BitdriftColors.TextPrimary,
108+
)
109+
110+
TerminationButton(
111+
text = "Native Crash",
112+
onClick = { onAction(DiagnosticsAction.TriggerRandomNativeCrash) },
113+
containerColor = BitdriftColors.CrashNative,
114+
)
115+
116+
TerminationButton(
117+
text = "JVM crash",
118+
onClick = { onAction(DiagnosticsAction.TriggerRandomJvmCrash) },
119+
containerColor = BitdriftColors.CrashJvm,
120+
)
121+
122+
TerminationButton(
123+
text = "ANR crash",
124+
onClick = { onAction(DiagnosticsAction.TriggerRandomAnrCrash) },
125+
containerColor = BitdriftColors.CrashAnr,
126+
)
127+
}
128+
}
129+
}
130+
131+
@Composable
132+
private fun TerminationButton(
133+
text: String,
134+
onClick: () -> Unit,
135+
containerColor: Color,
136+
) {
137+
Button(
138+
onClick = onClick,
139+
colors =
140+
ButtonDefaults.buttonColors(
141+
containerColor = containerColor,
142+
contentColor = BitdriftColors.TextBright,
143+
),
144+
modifier = Modifier.fillMaxWidth(),
145+
) {
146+
Text(text = text, color = Color.White)
147+
}
148+
}

platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ui/compose/components/TestingToolsCard.kt

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ import androidx.compose.ui.res.stringResource
1818
import androidx.compose.ui.unit.dp
1919
import io.bitdrift.capture.LogLevel
2020
import io.bitdrift.gradletestapp.R
21-
import io.bitdrift.gradletestapp.data.model.AppAction
2221
import io.bitdrift.gradletestapp.data.model.AppExitReason
2322
import io.bitdrift.gradletestapp.data.model.AppState
24-
import io.bitdrift.gradletestapp.data.model.DiagnosticsAction
2523
import io.bitdrift.gradletestapp.ui.theme.BitdriftColors
2624

2725
/**
@@ -31,9 +29,7 @@ import io.bitdrift.gradletestapp.ui.theme.BitdriftColors
3129
fun TestingToolsCard(
3230
uiState: AppState,
3331
onLogLevelChange: (LogLevel) -> Unit,
34-
onAppExitReasonChange: (AppExitReason) -> Unit,
3532
onLogMessage: () -> Unit,
36-
onAction: (AppAction) -> Unit,
3733
modifier: Modifier = Modifier,
3834
) {
3935
Card(
@@ -76,23 +72,6 @@ fun TestingToolsCard(
7672
) {
7773
Text("Log Message")
7874
}
79-
80-
AppExitReasonSelector(
81-
selectedAppExitReason = uiState.diagnostics.selectedAppExitReason,
82-
onAppExitReasonChange = onAppExitReasonChange,
83-
)
84-
85-
Button(
86-
onClick = { onAction(DiagnosticsAction.ForceAppExit) },
87-
colors =
88-
ButtonDefaults.buttonColors(
89-
containerColor = BitdriftColors.Error,
90-
contentColor = BitdriftColors.TextBright,
91-
),
92-
modifier = Modifier.fillMaxWidth(),
93-
) {
94-
Text("Force App Exit")
95-
}
9675
}
9776
}
9877
}
@@ -163,7 +142,7 @@ private fun LogLevelSelector(
163142

164143
@OptIn(ExperimentalMaterial3Api::class)
165144
@Composable
166-
private fun AppExitReasonSelector(
145+
fun AppExitReasonSelector(
167146
selectedAppExitReason: AppExitReason,
168147
onAppExitReasonChange: (AppExitReason) -> Unit,
169148
) {

platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ui/theme/Color.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ object BitdriftColors {
2222

2323
val Error = Color.Red
2424
val Warning = Color(0xFFFFA500)
25+
val CrashNative = Color(0xFFE11D48)
26+
val CrashJvm = Color(0xFFF59E0B)
27+
val CrashAnr = Color(0xFF8B5CF6)
2528
}

platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ui/viewmodel/MainViewModel.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ class MainViewModel(
111111

112112
is DiagnosticsAction.LogMessage -> logMessage()
113113
is DiagnosticsAction.ForceAppExit -> forceAppExit()
114+
is DiagnosticsAction.TriggerRandomNativeCrash -> triggerRandomNativeCrash()
115+
is DiagnosticsAction.TriggerRandomJvmCrash -> triggerRandomJvmCrash()
116+
is DiagnosticsAction.TriggerRandomAnrCrash -> triggerRandomAnrCrash()
114117
is DiagnosticsAction.UpdateAppExitReason -> updateAppExitReason(action.reason)
115118

116119
is NetworkTestAction.PerformOkHttpRequestManual -> {
@@ -348,6 +351,42 @@ class MainViewModel(
348351
}, "fatal-issue-trigger").start()
349352
}
350353

354+
private fun triggerRandomNativeCrash() {
355+
triggerRandomFatalIssue(
356+
label = "native",
357+
reasons = NATIVE_CRASH_REASONS,
358+
threadName = "native-fatal-issue-trigger",
359+
)
360+
}
361+
362+
private fun triggerRandomJvmCrash() {
363+
triggerRandomFatalIssue(
364+
label = "JVM",
365+
reasons = JVM_CRASH_REASONS,
366+
threadName = "jvm-fatal-issue-trigger",
367+
)
368+
}
369+
370+
private fun triggerRandomAnrCrash() {
371+
triggerRandomFatalIssue(
372+
label = "ANR",
373+
reasons = ANR_CRASH_REASONS,
374+
threadName = "anr-fatal-issue-trigger",
375+
)
376+
}
377+
378+
private fun triggerRandomFatalIssue(
379+
label: String,
380+
reasons: List<AppExitReason>,
381+
threadName: String,
382+
) {
383+
val reason = reasons.random()
384+
Timber.i("Triggering random $label fatal issue with reason: $reason")
385+
Thread({
386+
appExitRepository.triggerAppExit(application, reason)
387+
}, threadName).start()
388+
}
389+
351390
private fun updateApiKey(apiKey: String) {
352391
_uiState.update {
353392
it.copy(config = it.config.copy(apiKey = apiKey))
@@ -399,3 +438,28 @@ class MainViewModel(
399438
}
400439
}
401440
}
441+
442+
private val NATIVE_CRASH_REASONS =
443+
listOf(
444+
AppExitReason.NATIVE_CAPTURE_DESTROY_CRASH,
445+
AppExitReason.NATIVE_SIGSEGV,
446+
AppExitReason.NATIVE_SIGBUS,
447+
)
448+
449+
private val JVM_CRASH_REASONS =
450+
listOf(
451+
AppExitReason.APP_CRASH_COROUTINE_EXCEPTION,
452+
AppExitReason.APP_CRASH_REGULAR_JVM_EXCEPTION,
453+
AppExitReason.APP_CRASH_RX_JAVA_EXCEPTION,
454+
AppExitReason.APP_CRASH_OUT_OF_MEMORY,
455+
)
456+
457+
private val ANR_CRASH_REASONS =
458+
listOf(
459+
AppExitReason.ANR_BLOCKING_GET,
460+
AppExitReason.ANR_BROADCAST_RECEIVER,
461+
AppExitReason.ANR_COROUTINES,
462+
AppExitReason.ANR_DEADLOCK,
463+
AppExitReason.ANR_GENERIC,
464+
AppExitReason.ANR_SLEEP_MAIN_THREAD,
465+
)

0 commit comments

Comments
 (0)