11package io.sentry.samples.android.sqlite
22
33import android.os.Bundle
4+ import android.util.Log
45import android.widget.Toast
56import androidx.activity.ComponentActivity
67import androidx.activity.compose.setContent
@@ -33,6 +34,9 @@ import androidx.compose.material3.MaterialTheme
3334import androidx.compose.material3.OutlinedTextField
3435import androidx.compose.material3.OutlinedTextFieldDefaults
3536import androidx.compose.material3.PlainTooltip
37+ import androidx.compose.material3.SegmentedButton
38+ import androidx.compose.material3.SegmentedButtonDefaults
39+ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
3640import androidx.compose.material3.Surface
3741import androidx.compose.material3.Switch
3842import androidx.compose.material3.SwitchColors
@@ -73,6 +77,7 @@ import kotlinx.coroutines.withContext
7377
7478private val SentryPink = Color (0xFFC85B9C )
7579private val SentryPurple = Color (0xFF7B52FB )
80+ private val SentryOrange = Color (0xFFE8743F )
7681private val SentryRed = Color (0xFFF55459 )
7782
7883/* * Intro text, surfaced via the "?" tooltip next to the "Run it" header. */
@@ -88,10 +93,33 @@ private val CONTROL_SECTION_GAP = TOGGLE_SECTION_GAP * 2
8893
8994private val SECTION_HEADER_HEIGHT = 28 .dp
9095
91- /* * Which sentry-android-sqlite integration the demo buttons currently target. */
92- private enum class Integration (val color : Color , val apiName : String ) {
93- DRIVER (SentryPurple , " SQLiteDriver" ),
94- OPEN_HELPER (SentryPink , " SupportSQLiteOpenHelper" ),
96+ /* * Which sentry-android-sqlite integration the demo currently targets. */
97+ private enum class IntegrationMode (
98+ val color : Color ,
99+ val segmentLabel : String ,
100+ val apiName : String ,
101+ val subtitle : String ,
102+ ) {
103+ DRIVER (
104+ SentryPurple ,
105+ " SQLiteDriver" ,
106+ " SQLiteDriver" ,
107+ " SentrySQLiteDriver.create(BundledSQLiteDriver)" ,
108+ ),
109+ OPEN_HELPER (
110+ SentryPink ,
111+ " OpenHelper" ,
112+ " SupportSQLiteOpenHelper" ,
113+ " SentrySupportSQLiteOpenHelper.create(...)" ,
114+ ),
115+ // Not directly-supported, but lets us verify behavior when both the DRIVER and OPEN_HELPER
116+ // integrations are used together via the SupportSQLiteDriver bridge.
117+ BRIDGE (
118+ SentryOrange ,
119+ " Bridge" ,
120+ " SupportSQLiteDriver bridge" ,
121+ " SentrySQLiteDriver.create(SupportSQLiteDriver(Sentry helper))" ,
122+ ),
95123}
96124
97125/* *
@@ -107,11 +135,24 @@ private class DemoVariant(
107135)
108136
109137/* *
110- * A single demo button in the list. [driver] / [openHelper] hold the variant for each integration;
111- * a null variant means the row doesn't apply to that integration and renders dimmed, explaining why
112- * on click (Room 3 is driver-only; SQLDelight is open-helper-only).
138+ * A single demo button in the list. [driver] / [openHelper] / [bridge] hold the variant for each
139+ * integration; a null variant means the row doesn't apply and renders dimmed (e.g., Room 3 is
140+ * driver-only; SQLDelight is open-helper-only; etc. ).
113141 */
114- private class DemoRow (val label : String , val driver : DemoVariant ? , val openHelper : DemoVariant ? )
142+ private class DemoRow (
143+ val label : String ,
144+ val driver : DemoVariant ? ,
145+ val openHelper : DemoVariant ? ,
146+ val bridge : DemoVariant ? ,
147+ ) {
148+
149+ fun variantFor (mode : IntegrationMode ): DemoVariant ? =
150+ when (mode) {
151+ IntegrationMode .DRIVER -> driver
152+ IntegrationMode .OPEN_HELPER -> openHelper
153+ IntegrationMode .BRIDGE -> bridge
154+ }
155+ }
115156
116157// The demo buttons, top to bottom, paired with each integration's variant. Pure data — the actual
117158// SQL lives in SqlStatements, dispatched by id.
@@ -133,6 +174,13 @@ private val DEMO_ROWS =
133174 op = " db.sql.openhelper-direct" ,
134175 displayInfo = OPENHELPER_DIRECT ,
135176 ),
177+ bridge =
178+ DemoVariant (
179+ demo = SqlDemo .BRIDGE_DIRECT ,
180+ transactionName = " Bridge stack — Direct" ,
181+ op = " db.sql.bridge-direct" ,
182+ displayInfo = BRIDGE_DIRECT ,
183+ ),
136184 ),
137185 DemoRow (
138186 label = " Room 2" ,
@@ -150,6 +198,13 @@ private val DEMO_ROWS =
150198 op = " db.sql.openhelper-room" ,
151199 displayInfo = OPENHELPER_ROOM ,
152200 ),
201+ bridge =
202+ DemoVariant (
203+ demo = SqlDemo .BRIDGE_ROOM2 ,
204+ transactionName = " Bridge stack — Room 2" ,
205+ op = " db.sql.bridge-room2" ,
206+ displayInfo = BRIDGE_ROOM2 ,
207+ ),
153208 ),
154209 DemoRow (
155210 label = " Room 3" ,
@@ -161,6 +216,7 @@ private val DEMO_ROWS =
161216 displayInfo = DRIVER_ROOM3 ,
162217 ),
163218 openHelper = null , // Room 3 only runs on the SQLiteDriver path.
219+ bridge = null ,
164220 ),
165221 DemoRow (
166222 label = " SQLDelight" ,
@@ -172,6 +228,7 @@ private val DEMO_ROWS =
172228 op = " db.sql.openhelper-sqldelight" ,
173229 displayInfo = OPENHELPER_SQLDELIGHT ,
174230 ),
231+ bridge = null ,
175232 ),
176233 )
177234
@@ -187,6 +244,7 @@ private val DEMO_ROWS =
187244class SQLiteActivity : ComponentActivity () {
188245
189246 private var latestResult by mutableStateOf(" " )
247+ private var warmUpErrors by mutableStateOf(" " )
190248 private var sqlDetail by mutableStateOf(SQL_DETAIL_HINT )
191249 private var heavyWork by mutableStateOf(false )
192250
@@ -198,8 +256,8 @@ class SQLiteActivity : ComponentActivity() {
198256 */
199257 private var shareScreenTrace by mutableStateOf(false )
200258
201- /* * Which integration the demo buttons target . Switching it disables the rows that don't apply. */
202- private var integration by mutableStateOf(Integration .DRIVER )
259+ /* * Which integration is currently being demoed . Switching it disables rows that don't apply. */
260+ private var integration by mutableStateOf(IntegrationMode .DRIVER )
203261
204262 /* * Incremented on each tap that runs SQL. Used to retrigger the detail box's outline shimmer. */
205263 private var runTick by mutableStateOf(0 )
@@ -265,31 +323,19 @@ class SQLiteActivity : ComponentActivity() {
265323
266324 SectionHeader (" Configure it" )
267325
268- val openHelper = integration == Integration .OPEN_HELPER
269- val integrationSwitchColors =
270- SwitchDefaults .colors(
271- checkedTrackColor = SentryPink ,
272- checkedBorderColor = SentryPink ,
273- uncheckedTrackColor = SentryPurple ,
274- uncheckedBorderColor = SentryPurple ,
275- uncheckedThumbColor = Color .White ,
276- )
277326 val controlSwitchColors =
278327 SwitchDefaults .colors(
279328 checkedTrackColor = Color .Black ,
280329 checkedBorderColor = Color .Black ,
281330 )
282- ToggleRow (
283- label = if (openHelper) " SentrySupportSQLiteOpenHelper" else " SentrySQLiteDriver" ,
284- checked = openHelper,
285- labelColor = if (openHelper) SentryPink else SentryPurple ,
286- switchColors = integrationSwitchColors,
287- ) {
288- integration = if (it) Integration .OPEN_HELPER else Integration .DRIVER
289- // Switching integration starts a fresh comparison: clear the detail box and result.
290- sqlDetail = SQL_DETAIL_HINT
291- latestResult = " "
292- }
331+ IntegrationModeSelector (
332+ selected = integration,
333+ onSelected = {
334+ integration = it
335+ sqlDetail = SQL_DETAIL_HINT
336+ latestResult = " "
337+ },
338+ )
293339 ToggleRow (
294340 label = if (heavyWork) " Heavy app-level work" else " No app-level work" ,
295341 checked = heavyWork,
@@ -313,12 +359,12 @@ class SQLiteActivity : ComponentActivity() {
313359 // integration's variant; a row that doesn't apply explains why via a toast (see
314360 // [DemoRowButton]).
315361 DEMO_ROWS .forEach { row ->
316- val variant = if (integration == Integration . DRIVER ) row.driver else row.openHelper
362+ val variant = row.variantFor (integration)
317363 DemoRowButton (
318364 label = row.label,
319365 color = integration.color,
320366 variant = variant,
321- disabledReason = " ${row.label} doesn't use the ${integration.apiName} " ,
367+ disabledReason = " ${row.label} doesn't apply to the ${integration.apiName} stack " ,
322368 )
323369 }
324370
@@ -330,12 +376,26 @@ class SQLiteActivity : ComponentActivity() {
330376 // Same [CONTROL_SECTION_GAP] above as the other sections, separating the controls from
331377 // the detail output.
332378 SectionHeader (" Under the hood" , topPadding = CONTROL_SECTION_GAP )
379+ LaunchedEffect (Unit ) {
380+ while (! SampleDatabases .isWarmUpComplete()) {
381+ warmUpErrors = SampleDatabases .warmUpErrors
382+ delay(250 )
383+ }
384+ warmUpErrors = SampleDatabases .warmUpErrors
385+ }
386+ if (warmUpErrors.isNotEmpty()) {
387+ Text (
388+ text = warmUpErrors,
389+ style = MaterialTheme .typography.bodyMedium,
390+ color = SentryRed ,
391+ )
392+ }
333393 // The latest run result (row counts, errors). Hidden until the first run.
334394 if (latestResult.isNotEmpty()) {
335395 Text (
336396 text = latestResult,
337397 style = MaterialTheme .typography.bodyMedium,
338- color = if (latestResult.contains( " failed " )) SentryRed else Color .Unspecified ,
398+ color = if (latestResult.looksLikeError( )) SentryRed else Color .Unspecified ,
339399 )
340400 }
341401 DetailField (" SQL run" , sqlDetail, borderColor = detailOutline)
@@ -361,12 +421,13 @@ class SQLiteActivity : ComponentActivity() {
361421 lifecycleScope.launch {
362422 dbOperationInFlight = true
363423 try {
364- latestResult =
424+ val result =
365425 withContext(Dispatchers .IO ) {
366426 runInTransaction(variant.transactionName, variant.op) {
367427 SqlStatements .execute(applicationContext, variant.demo, heavyWork)
368428 }
369429 }
430+ latestResult = result
370431 } finally {
371432 dbOperationInFlight = false
372433 }
@@ -385,9 +446,41 @@ class SQLiteActivity : ComponentActivity() {
385446 startActivity(UiLoadActivity .intent(this , variant.demo, heavyWork))
386447 }
387448
449+ @OptIn(ExperimentalMaterial3Api ::class )
450+ @androidx.compose.runtime.Composable
451+ private fun IntegrationModeSelector (
452+ selected : IntegrationMode ,
453+ onSelected : (IntegrationMode ) -> Unit ,
454+ ) {
455+ SingleChoiceSegmentedButtonRow (modifier = Modifier .fillMaxWidth()) {
456+ IntegrationMode .entries.forEachIndexed { index, mode ->
457+ SegmentedButton (
458+ shape =
459+ SegmentedButtonDefaults .itemShape(index = index, count = IntegrationMode .entries.size),
460+ onClick = { onSelected(mode) },
461+ selected = selected == mode,
462+ icon = {},
463+ colors =
464+ SegmentedButtonDefaults .colors(
465+ activeContainerColor = mode.color,
466+ activeContentColor = Color .White ,
467+ ),
468+ label = { Text (mode.segmentLabel, style = MaterialTheme .typography.labelSmall) },
469+ )
470+ }
471+ }
472+
473+ Text (
474+ text = selected.subtitle,
475+ style = MaterialTheme .typography.bodySmall,
476+ color = Color .Gray ,
477+ modifier = Modifier .padding(top = 6 .dp),
478+ )
479+ }
480+
388481 /* *
389482 * A compact, left-justified labeled switch. [labelColor] defaults to [Color.Unspecified] so the
390- * label inherits the default text color; the integration toggle passes its pink/purple instead .
483+ * label inherits the default text color.
391484 */
392485 @androidx.compose.runtime.Composable
393486 private fun ToggleRow (
@@ -533,7 +626,11 @@ class SQLiteActivity : ComponentActivity() {
533626 try {
534627 val message = withContext(Dispatchers .IO ) { resetDatabases() }
535628 latestResult = message
629+ warmUpErrors = SampleDatabases .warmUpErrors
536630 sqlDetail = " DROP: deletes every demo database file, resetting all row counts to 0."
631+ } catch (t: Throwable ) {
632+ Log .e(TAG , " Reset failed" , t)
633+ latestResult = " Reset failed: ${t.message ? : t.javaClass.simpleName} "
537634 } finally {
538635 this @SQLiteActivity.dbOperationInFlight = false
539636 this @SQLiteActivity.resetInProgress = false
@@ -595,7 +692,8 @@ class SQLiteActivity : ComponentActivity() {
595692 result
596693 } catch (t: Throwable ) {
597694 transaction.status = SpanStatus .INTERNAL_ERROR
598- " $transactionName failed: ${t.message} "
695+ Log .e(TAG , " $transactionName failed" , t)
696+ " $transactionName failed: ${t.message ? : t.javaClass.simpleName} "
599697 } finally {
600698 transaction.finish()
601699 }
@@ -604,11 +702,20 @@ class SQLiteActivity : ComponentActivity() {
604702 /* * Closes + deletes every demo database file (via [SampleDatabases]), then re-warms them. */
605703 private suspend fun resetDatabases (): String {
606704 val cleared = SampleDatabases .reset(applicationContext)
607- return " Dropped tables: cleared $cleared database file(s)."
705+ SampleDatabases .awaitWarmUp()
706+ return buildString {
707+ append(" Dropped tables: cleared $cleared database file(s)." )
708+ if (SampleDatabases .warmUpErrors.isNotEmpty()) {
709+ append(" \n\n " )
710+ append(SampleDatabases .warmUpErrors)
711+ }
712+ }
608713 }
609714
610715 private companion object {
611716
717+ private const val TAG = " SQLiteActivity"
718+
612719 /* * Demo SQL shorter than this won't visibly disable the reset button. */
613720 private const val RESET_DISABLE_DEBOUNCE_MS = 300L
614721
@@ -619,3 +726,5 @@ class SQLiteActivity : ComponentActivity() {
619726 private fun newScreenTrace (): String = " ${SentryId ()} -${SpanId ()} -1"
620727 }
621728}
729+
730+ private fun String.looksLikeError (): Boolean = contains(" failed" , ignoreCase = true )
0 commit comments