Output:
Chunk ID: 1456f9
Wall time: 0.3549 seconds
Process exited with code 0
Original token count: 2675
Output:
package com.yorvana.ui.settings

import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.yorvana.BuildConfig
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.data.billing.DebugBillingActions
import com.yorvana.data.billing.RestoreResult
import com.yorvana.data.model.OdometerUnit
import com.yorvana.data.preferences.DebugBillingOverrideMode
import com.yorvana.data.storage.DestinationNotEmptyException
import com.yorvana.ui.util.GatedViewModel
import com.yorvana.ui.util.ViewModelDependencies
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

data class SettingsState(
    val vaultUriString: String? = null,
    val defaultOdometerUnit: OdometerUnit = OdometerUnit.KM,
    val defaultCurrency: String = "USD",
    val appVersion: String = "",
    val isValidating: Boolean = false,
    val isMigrating: Boolean = false,
    val showMoveOrFreshDialog: Boolean = false,
    val showConflictDialog: Boolean = false,
    val showUseExistingVaultDialog: Boolean = false,
    val pendingNewUri: Uri? = null,
    val isPremium: Boolean = false,
    val isPremiumPending: Boolean = false,
    /** Added to feed Phase 5 UI (disabling buttons/showing banners); logic gating is in VM. */
    val isReadOnly: Boolean = false,
    val isRestoringPurchases: Boolean = false,
    val debugOverrideMode: DebugBillingOverrideMode = DebugBillingOverrideMode.NONE,
    val crashReportingEnabled: Boolean = false,
)

sealed class SettingsEvent {
    data object OpenCategories : SettingsEvent()

    data object OpenReportBug : SettingsEvent()

    data object OpenContact : SettingsEvent()

    data object OpenFileManager : SettingsEvent()

    data class VaultFolderPicked(val uri: Uri) : SettingsEvent()

    data object MoveVault : SettingsEvent()

    data object StartFresh : SettingsEvent()

    data object DismissMoveDialog : SettingsEvent()

    data object DismissConflictDialog : SettingsEvent()

    data object DismissUseExistingVaultDialog : SettingsEvent()

    data class OdometerUnitChanged(val unit: OdometerUnit) : SettingsEvent()

    data class CurrencyChanged(val currency: String) : SettingsEvent()

    data object RequestUpgrade : SettingsEvent()

    data object RestorePurchases : SettingsEvent()

    data object SimulatePurchaseSuccess : SettingsEvent()

    data object ResetBillingState : SettingsEvent()

    data class SetDebugOverride(val mode: DebugBillingOverrideMode) : SettingsEvent()

    data class CrashReportingToggled(val enabled: Boolean) : SettingsEvent()
}

sealed class SettingsEffect {
    data object NavigateToCategories : SettingsEffect()

    data object NavigateToReportBug : SettingsEffect()

    data class LaunchContactEmail(
        val recipient: String,
        val subject: String,
    ) : SettingsEffect()

    data class LaunchFileManager(val vaultUriString: String) : SettingsEffect()

    data class ShowSnackbar(val message: String) : SettingsEffect()

    data object LaunchPurchaseFlow : SettingsEffect()
}

/**
 * SettingsViewModel manages vault selection, preferences, and billing actions.
 *
 * **State architecture note**: two state holders coexist:
 * - `_state` (`MutableStateFlow<SettingsState>`) holds billing/internal fields (isPremium,
 *   isReadOnly, isRestoringPurchases, debugOverrideMode). Internal logic **must** read these
 *   via `_state.value` — the public `state` flow is `WhileSubscribed`-cached and returns
 *   `initialValue = SettingsState()` when there is no active subscriber.
 * - `state` (public `StateFlow`) is the `combine` of `_state` + preferences. Fields that
 *   originate from preferences (vaultUriString, defaultOdometerUnit, etc.) are **only**
 *   available via `state.value` since they flow exclusively through the combine.
 *
 * This distinction applies to every field added to [SettingsState] in the future.
 */
class SettingsViewModel(
    dependencies: ViewModelDependencies,
) : GatedViewModel(dependencies) {
    private val preferences = dependencies.preferences
    private val vaultStorage = dependencies.vaultStorage
    private val billingManager = dependencies.billingManager
    private val debugBillingOverride = dependencies.debugBillingOverride
    private val _state = MutableStateFlow(SettingsState())
    val state: StateFlow<SettingsState> =
        combine(
            _state,
            preferences.preferences,
        ) { internalState, prefs ->
            internalState.copy(
                vaultUriString = prefs.vaultUriString,
                defaultOdometerUnit = prefs.defaultOdometerUnit,
                defaultCurrency = prefs.defaultCurrency,
                appVersion = getAppVersion(),
                crashReportingEnabled = prefs.crashReportingEnabled,
            )
        }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS),
            initialValue = SettingsState(),
        )

    // BUFFERED so that effects emitted before the UI subscribes (e.g., during tests or when
    // the UI briefly unsubscribes) are not dropped rather than suspending the sender.
    private val _effects = Channel<SettingsEffect>(Channel.BUFFERED)
    val effects = _effects.receiveAsFlow()

    private var currencyChangeJob: Job? = null

    init {
        appGate.isPremium
            .onEach { premium -> _state.update { it.copy(isPremium = premium) } }
            .launchIn(viewModelScope)
        appGate.hasPendingPurchase
            .onEach { pending -> _state.update { it.copy(isPremiumPending = pending) } }
            .launchIn(viewModelScope)
        appGate.isReadOnly
            .onEach { readOnly -> _state.update { it.copy(isReadOnly = readOnly) } }
            .launchIn(viewModelScope)
        debugBillingOverride?.mode
            ?.onEach { mode -> _state.update { it.copy(debugOverrideMode = mode) } }
            ?.launchIn(viewModelScope)
    }

    private fun getAppVersion(): String {
        return try {
            val packageInfo = app.packageManager.getPackageInfo(app.packageName, 0)
            packageInfo.versionName ?: ""
        } catch (e: PackageManager.NameNotFoundException) {
            Log.w("SettingsViewModel", "Could not get app version", e)
            ""
        }
    }

    @Suppress("CyclomaticComplexMethod")
    fun onEvent(event: SettingsEvent) {
        when (event) {
            is SettingsEvent.VaultFolderPicked -> handleVaultPicked(event.uri)
            is SettingsEvent.OdometerUnitChanged ->
                viewModelScope.launch { preferences.setDefaultOdometerUnit(event.unit) }
            is SettingsEvent.CurrencyChanged -> {
                currencyChangeJob?.cancel()
                currencyChangeJob =
                    viewModelScope.launch {
                        delay(CURRENCY_CHANGE_DELAY_MILLIS)
                        preferences.setDefaultCurrency(event.currency)
                    }
            }
            SettingsEvent.MoveVault -> moveVault()
            SettingsEvent.StartFresh -> startFresh()
            SettingsEvent.DismissMoveDialog -> _state.update { it.copy(showMoveOrFreshDialog = false, pendingNewUri = null) }
            SettingsEvent.DismissConflictDialog -> _state.update { it.copy(showConflictDialog = false) }
            SettingsEvent.DismissUseExistingVaultDialog ->
                _state.update {
                    it.copy(
                        showUseExistingVaultDialog = false,
                        pendingNewUri = null,
                    )
                }
            SettingsEvent.OpenCategories -> {
                viewModelScope.launch { _effects.send(SettingsEffect.NavigateToCategories) }
            }
            SettingsEvent.OpenReportBug -> {
                viewModelScope.launch { _effects.send(SettingsEffect.NavigateToReportBug) }
            }
            SettingsEvent.OpenContact -> {
                viewModelScope.launch {
                    _effects.send(
                        SettingsEffect.LaunchContactEmail(
                            recipient = app.getString(R.string.report_bug_support_email),
                            subject = "[Yorvana] Contact",
                        ),
                    )
                }
            }
            SettingsEvent.OpenFileManager -> {
                viewModelScope.launch {
                    val uriString = preferences.preferences.first().vaultUriString ?: return@launch
                    _effects.send(SettingsEffect.LaunchFileManager(uriString))
                }
            }
            SettingsEvent.RequestUpgrade ->
                if (!appGate.isPremium.value) {
                    viewModelScope.launch { _effects.send(SettingsEffect.LaunchPurchaseFlow) }
                }
            SettingsEvent.RestorePurchases -> restorePurchases()
            SettingsEvent.SimulatePurchaseSuccess -> handleSimulatePurchaseSuccess()
            SettingsEvent.ResetBillingState -> handleResetBillingState()
            is SettingsEvent.SetDebugOverride -> handleSetDebugOverride(event.mode)
            is SettingsEvent.CrashReportingToggled -> handleCrashReportingToggled(event.enabled)
        }
    }

    private fun handleSimulatePurchaseSuccess() {
        if (BuildConfig.DEBUG) {
            viewModelScope.launch {
                (billingManager as? DebugBillingActions)?.simulatePurchaseSuccess()
                _effects.send(SettingsEffect.ShowSnackbar(app.getString(R.string.settings_debug_purchase_success)))
            }
        }
    }

    private fun handleResetBillingState() {
        if (BuildConfig.DEBUG) {
            viewModelScope.launch {
                (billingManager as? DebugBillingActions)?.resetBillingState()
                _effects.send(SettingsEffect.ShowSnackbar(app.getString(R.string.settings_debug_billing_reset)))
            }
        }
    }

    private fun handleSetDebugOverride(mode: DebugBillingOverrideMode) {
        if (BuildConfig.DEBUG) {
            viewModelScope.launch { debugBillingOverride?.setMode(mode) }
        } else {
            Log.w("SettingsViewModel", "SetDebugOverride called in non-debug build")


Output:
Chunk ID: 005888
Wall time: 0.4498 seconds
Process exited with code 0
Original token count: 2475
Output:
package com.yorvana.ui.settings

import android.content.ActivityNotFoundException
import android.content.Intent
import android.provider.DocumentsContract
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.automirrored.outlined.ArrowForwardIos
import androidx.compose.material.icons.filled.AttachMoney
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Category
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import com.yorvana.BuildConfig
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.data.model.OdometerUnit
import com.yorvana.data.preferences.DebugBillingOverrideMode
import com.yorvana.ui.TestTags
import com.yorvana.ui.components.CurrencyPickerDialog
import com.yorvana.ui.components.CustomCurrencyDialog
import com.yorvana.ui.components.DismissablePendingDialog
import com.yorvana.ui.components.PaywallScaffold
import com.yorvana.ui.components.PrivacyPolicyDialog
import com.yorvana.ui.components.TopBarBackButton
import com.yorvana.ui.navigation.Categories
import com.yorvana.ui.navigation.ReportBug
import com.yorvana.ui.theme.appTopAppBarColors
import com.yorvana.ui.theme.paywallContainerColor
import com.yorvana.ui.theme.paywallContentColor
import com.yorvana.ui.util.EmailLaunchRequest
import com.yorvana.ui.util.ViewModelFactory
import com.yorvana.ui.util.launchEmail
import com.yorvana.ui.util.rememberUpgradeLauncher
import com.yorvana.util.VaultPathHelper

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
    backStack: NavBackStack<NavKey>,
    viewModel: SettingsViewModel =
        viewModel(
            factory = ViewModelFactory(LocalContext.current.applicationContext as YorvanaApplication),
        ),
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    val context = LocalContext.current
    val snackbarHostState = remember { SnackbarHostState() }
    val isDark = isSystemInDarkTheme()
    val upgradeLauncher = rememberUpgradeLauncher(snackbarHostState)
    val fileManagerError = stringResource(R.string.settings_file_manager_error)
    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)

    var showOdometerDialog by remember { mutableStateOf(false) }
    var showCurrencyDialog by remember { mutableStateOf(false) }
    var showCustomCurrencyDialog by remember { mutableStateOf(false) }
    var showCrashConfirmation by remember { mutableStateOf(false) }
    var showPrivacyDialog by rememberSaveable { mutableStateOf(false) }

    if (showPrivacyDialog) {
        PrivacyPolicyDialog(onDismissRequest = { showPrivacyDialog = false })
    }

    val folderPicker =
        rememberLauncherForActivityResult(
            ActivityResultContracts.OpenDocumentTree(),
        ) { uri ->
            if (uri == null) return@rememberLauncherForActivityResult
            context.contentResolver.takePersistableUriPermission(
                uri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
            )
            viewModel.onEvent(SettingsEvent.VaultFolderPicked(uri))
        }

    LaunchedEffect(Unit) {
        viewModel.effects.collect { effect ->
            when (effect) {
                SettingsEffect.NavigateToCategories ->
                    backStack.add(Categories)
                SettingsEffect.NavigateToReportBug ->
                    backStack.add(ReportBug)
                is SettingsEffect.LaunchContactEmail -> {
                    launchEmail(
                        context = context,
                        snackbarHostState = snackbarHostState,
                        noEmailAppMessage = noEmailAppMessage,
                        request =
                            EmailLaunchRequest(
                                recipient = effect.recipient,
                                subject = effect.subject,
                            ),
                    )
                }
                is SettingsEffect.ShowSnackbar ->
                    snackbarHostState.showSnackbar(effect.message)
                is SettingsEffect.LaunchFileManager -> {
                    try {
                        val treeUri = android.net.Uri.parse(effect.vaultUriString)
                        val docUri =
                            DocumentsContract.buildDocumentUriUsingTree(
                                treeUri,
                                DocumentsContract.getTreeDocumentId(treeUri),
                            )
                        val intent =
                            Intent(Intent.ACTION_VIEW).apply {
                                setDataAndType(docUri, DocumentsContract.Document.MIME_TYPE_DIR)
                                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                            }
                        context.startActivity(intent)
                    } catch (_: ActivityNotFoundException) {
                        snackbarHostState.showSnackbar(fileManagerError)
                    }
                }
                SettingsEffect.LaunchPurchaseFlow -> {
                    upgradeLauncher()
                }
            }
        }
    }

    if (state.showMoveOrFreshDialog) {
        AlertDialog(
            onDismissRequest = { viewModel.onEvent(SettingsEvent.DismissMoveDialog) },
            title = { Text(stringResource(R.string.settings_vault_move_title)) },
            text = { Text(stringResource(R.string.settings_vault_move_message)) },
            confirmButton = {
                TextButton(onClick = { viewModel.onEvent(SettingsEvent.MoveVault) }) {
                    Text(stringResource(R.string.settings_vault_move_action))
                }
            },
            dismissButton = {
                Row {
                    TextButton(onClick = { viewModel.onEvent(SettingsEvent.StartFresh) }) {
                        Text(stringResource(R.string.settings_vault_fresh_action))
                    }
                    TextButton(onClick = { viewModel.onEvent(SettingsEvent.DismissMoveDialog) }) {
                        Text(stringResource(R.string.action_cancel))
                    }
                }
            },
        )
    }

    if (state.isValidating) {
        AlertDialog(
            onDismissRequest = { },
            confirmButton = { },
            title = {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    CircularProgressIndicator(modifier = Modifier.size(24.dp))
                    Spacer(Modifier.size(16.dp))
                    Text(stringResource(R.string.settings_vault_validating))
                }
            },
        )
    }

    if (state.showUseExistingVaultDialog) {
        AlertDialog(
            onDismissRequest = { viewModel.onEvent(SettingsEvent.DismissUseExistingVaultDialog) },
            title = { Text(stringResource(R.string.settings_vault_existing_title)) },
            text = { Text(stringResource(R.string.settings_vault_existing_message)) },
            confirmButton = {


Output:
Chunk ID: 4a4768
Wall time: 0.4424 seconds
Process exited with code 0
Original token count: 1903
Output:
package com.yorvana.ui.reportbug

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.ui.TestTags
import com.yorvana.ui.components.TopBarBackButton
import com.yorvana.ui.theme.appTopAppBarColors
import com.yorvana.ui.util.EmailLaunchRequest
import com.yorvana.ui.util.ViewModelFactory
import com.yorvana.ui.util.launchEmail

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReportBugScreen(
    backStack: NavBackStack<NavKey>,
    viewModel: ReportBugViewModel =
        viewModel(
            factory = ViewModelFactory(LocalContext.current.applicationContext as YorvanaApplication),
        ),
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    val context = LocalContext.current
    val snackbarHostState = remember { SnackbarHostState() }
    val scrollState = rememberScrollState()

    var showDeviceInfo by rememberSaveable { mutableStateOf(false) }

    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)

    LaunchedEffect(Unit) {
        viewModel.effects.collect { effect ->
            when (effect) {
                is ReportBugEffect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.message)
                ReportBugEffect.NavigateBack -> backStack.removeLastOrNull()
                is ReportBugEffect.FallbackToEmail -> {
                    launchEmail(
                        context = context,
                        snackbarHostState = snackbarHostState,
                        noEmailAppMessage = noEmailAppMessage,
                        request =
                            EmailLaunchRequest(
                                recipient = effect.recipient,
                                subject = effect.subject,
                                body = effect.body,
                            ),
                    )
                }
            }
        }
    }

    if (showDeviceInfo) {
        DeviceInfoDialog(
            state = state,
            onIncludeChanged = { viewModel.onEvent(ReportBugEvent.IncludeDeviceInfoChanged(it)) },
            onDismissRequest = { showDeviceInfo = false },
        )
    }

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) },
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = stringResource(R.string.report_bug_title),
                        modifier = Modifier.testTag(TestTags.SCREEN_TITLE),
                    )
                },
                navigationIcon = {
                    TopBarBackButton(
                        onClick = { backStack.removeLastOrNull() },
                        enabled = !state.isSubmitting,
                    )
                },
                actions = {
                    if (state.isSubmitting) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(24.dp).padding(end = 16.dp).testTag(TestTags.REPORT_BUG_LOADING),
                            strokeWidth = 2.dp,
                        )
                    } else {
                        TextButton(
                            onClick = { viewModel.onEvent(ReportBugEvent.Submit) },
                            enabled = state.canSubmit,
                            modifier = Modifier.testTag(TestTags.REPORT_BUG_SUBMIT_BUTTON),
                        ) {
                            Text(stringResource(R.string.report_bug_submit))
                        }
                    }
                },
                colors = appTopAppBarColors(),
            )
        },
    ) { padding ->
        Column(
            modifier =
                Modifier
                    .fillMaxSize()
                    .padding(padding)
                    .imePadding()
                    .verticalScroll(scrollState)
                    .padding(horizontal = 16.dp, vertical = 12.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            if (state.isSubmitting) {
                LinearProgressIndicator(
                    modifier = Modifier.fillMaxWidth(),
                )
            }

            // Type
            Text(
                text = stringResource(R.string.report_bug_type_section),
                style = MaterialTheme.typography.titleSmall,
                color = MaterialTheme.colorScheme.primary,
            )

            SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
                SegmentedButton(
                    selected = state.type == ReportType.BUG,
                    onClick = { viewModel.onEvent(ReportBugEvent.TypeChanged(ReportType.BUG)) },
                    shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
                    enabled = !state.isSubmitting,
                    label = { Text(stringResource(R.string.report_bug_type_bug)) },
                    modifier = Modifier.testTag(TestTags.REPORT_BUG_TYPE_BUG),
                )
                SegmentedButton(
                    selected = state.type == ReportType.SUGGESTION,
                    onClick = { viewModel.onEvent(ReportBugEvent.TypeChanged(ReportType.SUGGESTION)) },


Output:
Chunk ID: 6c54a3
Wall time: 0.4384 seconds
Process exited with code 0
Original token count: 252
Output:
package com.yorvana.ui.util

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.compose.material3.SnackbarHostState
import androidx.core.net.toUri

data class EmailLaunchRequest(
    val recipient: String,
    val subject: String,
    val body: String? = null,
)

suspend fun launchEmail(
    context: Context,
    snackbarHostState: SnackbarHostState,
    noEmailAppMessage: String,
    request: EmailLaunchRequest,
) {
    val intent =
        Intent(Intent.ACTION_SENDTO).apply {
            data = "mailto:".toUri()
            putExtra(Intent.EXTRA_EMAIL, arrayOf(request.recipient))
            putExtra(Intent.EXTRA_SUBJECT, request.subject)
            if (request.body != null) {
                putExtra(Intent.EXTRA_TEXT, request.body)
            }
        }
    try {
        context.startActivity(intent)
    } catch (_: ActivityNotFoundException) {
        snackbarHostState.showSnackbar(noEmailAppMessage)
    }
}


Output:
Chunk ID: dfc113
Wall time: 0.4361 seconds
Process exited with code 0
Original token count: 1087
Output:
app/src/main/res/values/strings.xml:110:    <string name="settings_contact">Contact</string>
app/src/main/res/values/strings.xml:111:    <string name="settings_contact_subtitle">Email Yorvana support</string>
app/src/main/res/values/strings.xml:121:    <string name="report_bug_contact_email">Contact email (optional)</string>
app/src/main/res/values/strings.xml:123:    <string name="report_bug_email_contact_label">Contact email</string>
app/src/main/res/values/strings.xml:125:    <string name="report_bug_no_email_app">No email app found</string>
app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt:57:    data object OpenContact : SettingsEvent()
app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt:95:    data class LaunchContactEmail(
app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt:210:            SettingsEvent.OpenContact -> {
app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt:213:                        SettingsEffect.LaunchContactEmail(
app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt:215:                            subject = "[Yorvana] Contact",
app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt:108:    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)
app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt:139:                is SettingsEffect.LaunchContactEmail -> {
app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt:709:                    headlineContent = { Text(stringResource(R.string.settings_contact)) },
app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt:710:                    supportingContent = { Text(stringResource(R.string.settings_contact_subtitle)) },
app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt:718:                    modifier = Modifier.clickable { viewModel.onEvent(SettingsEvent.OpenContact) },
app/src/test/java/com/yorvana/ui/settings/SettingsScreenIT.kt:98:    fun clickContact_triggersOpenContactEvent() {
app/src/test/java/com/yorvana/ui/settings/SettingsScreenIT.kt:120:        composeTestRule.onNodeWithText("Contact").performScrollTo().performClick()
app/src/test/java/com/yorvana/ui/settings/SettingsScreenIT.kt:122:        verify { mockViewModel.onEvent(SettingsEvent.OpenContact) }
app/src/main/java/com/yorvana/ui/reportbug/ReportBugViewModel.kt:60:    data class ContactEmailChanged(val email: String) : ReportBugEvent()
app/src/main/java/com/yorvana/ui/reportbug/ReportBugViewModel.kt:117:            is ReportBugEvent.ContactEmailChanged -> _state.update { it.copy(contactEmail = event.email) }
app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt:78:    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)
app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt:223:                onValueChange = { viewModel.onEvent(ReportBugEvent.ContactEmailChanged(it)) },
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:151:    fun `onEvent OpenContact should emit LaunchContactEmail effect`() =
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:156:                viewModel.onEvent(SettingsEvent.OpenContact)
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:159:                        SettingsEffect.LaunchContactEmail(
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:161:                            subject = "[Yorvana] Contact",
app/src/test/java/com/yorvana/ui/reportbug/ReportBugScreenTest.kt:145:        viewModel.onEvent(ReportBugEvent.ContactEmailChanged("invalid"))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:99:            viewModel.onEvent(ReportBugEvent.ContactEmailChanged("invalid-email"))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:103:            viewModel.onEvent(ReportBugEvent.ContactEmailChanged("a@b"))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:106:            viewModel.onEvent(ReportBugEvent.ContactEmailChanged("test@example.com"))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:147:            viewModel.onEvent(ReportBugEvent.ContactEmailChanged("test@example.com"))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:295:            viewModel.onEvent(ReportBugEvent.ContactEmailChanged("test@example.com"))


sed -n '130,180p' app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt


sed -n '90,135p' app/src/main/res/values/strings.xml


sed -n '1,70p' app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt


rg -n "getString\(|report_bug_no_email_app|settings_contact_email_subject" app/src/test


Output:
Chunk ID: 17ce0f
Wall time: 0.3889 seconds
Process exited with code 0
Original token count: 434
Output:
        runTest {
            viewModel = createViewModel()

            viewModel.onEvent(SettingsEvent.OdometerUnitChanged(OdometerUnit.MI))

            coVerify { preferences.setDefaultOdometerUnit(OdometerUnit.MI) }
        }

    @Test
    fun `onEvent OpenCategories should emit NavigateToCategories effect`() =
        runTest {
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.OpenCategories)
                assertThat(awaitItem()).isEqualTo(SettingsEffect.NavigateToCategories)
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent OpenContact should emit LaunchContactEmail effect`() =
        runTest {
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.OpenContact)
                assertThat(awaitItem())
                    .isEqualTo(
                        SettingsEffect.LaunchContactEmail(
                            recipient = "yorvana.app@gmail.com",
                            subject = "[Yorvana] Contact",
                        ),
                    )
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RequestUpgrade should emit LaunchPurchaseFlow when not premium`() =
        runTest {
            viewModel = createViewModel()
            // premiumState is false by default in setUp

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.RequestUpgrade)
                assertThat(awaitItem()).isEqualTo(SettingsEffect.LaunchPurchaseFlow)
                cancelAndIgnoreRemainingEvents()
            }
        }


Output:
Chunk ID: 324000
Wall time: 0.3549 seconds
Process exited with code 0
Original token count: 841
Output:
    <string name="settings_vault_move_action">Move data</string>
    <string name="settings_vault_fresh_action">Start fresh</string>
    <string name="settings_vault_moving">Moving vault…</string>
    <string name="settings_vault_move_error">Failed to move vault: %s</string>
    <string name="settings_vault_changed_success">Vault folder successfully changed.</string>
    <string name="settings_vault_conflict_title">Folder Not Empty</string>
    <string name="settings_vault_conflict_message">Cannot move vault: The selected folder is not empty and is not a Yorvana vault. Please choose an empty folder.</string>
    <string name="settings_vault_existing_title">Existing Vault Found</string>
    <string name="settings_vault_existing_message">This folder already contains Yorvana data. Would you like to use this existing vault?</string>
    <string name="settings_vault_use_existing_action">Use this vault</string>
    <string name="settings_vault_validating">Validating folder…</string>
    <string name="settings_preferences">Preferences</string>
    <string name="settings_odometer_unit">Default odometer unit</string>
    <string name="settings_currency">Default currency</string>
    <string name="settings_categories">Service categories</string>
    <string name="settings_unit_km">Kilometres (km)</string>
    <string name="settings_unit_mi">Miles (mi)</string>
    <string name="settings_explore_data">Explore Data</string>
    <string name="settings_explore_data_subtitle">Open vault in system file manager</string>
    <string name="settings_about">About</string>
    <string name="settings_contact">Contact</string>
    <string name="settings_contact_subtitle">Email Yorvana support</string>
    <string name="settings_report_bug">Report a bug or suggest a feature</string>
    <string name="settings_version">Version %s</string>

    <!-- Report Bug -->
    <string name="report_bug_title">Feedback</string>
    <string name="report_bug_type_bug">Bug</string>
    <string name="report_bug_type_suggestion">Suggestion</string>
    <string name="report_bug_summary">Summary</string>
    <string name="report_bug_description">Description</string>
    <string name="report_bug_contact_email">Contact email (optional)</string>
    <string name="report_bug_contact_email_invalid">Please enter a valid email address</string>
    <string name="report_bug_email_contact_label">Contact email</string>
    <string name="report_bug_type_section">Type</string>
    <string name="report_bug_no_email_app">No email app found</string>
    <string name="report_bug_include_device_info">Include additional app state</string>
    <string name="report_bug_device_info_payload">Device information</string>
    <string name="report_bug_sentry_opt_out_disclosure">Submitting will connect to our error reporting service for this report only. Basic device info is always attached.</string>
    <string name="report_bug_email_device_info">--- Device Info ---</string>
    <string name="report_bug_submit">Submit</string>
    <string name="report_bug_error_failed">Failed to send report after retries</string>
    <string name="report_bug_vault_configured">Vault Configured</string>
    <string name="report_bug_app_version">App Version</string>
    <string name="report_bug_android_version">Android Version</string>
    <string name="report_bug_locale">Locale</string>


Output:
Chunk ID: 13b74b
Wall time: 0.4021 seconds
Process exited with code 0
Original token count: 697
Output:
package com.yorvana.ui.settings

import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.util.Log
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.data.billing.BillingManager
import com.yorvana.data.billing.DebugBillingOverride
import com.yorvana.data.billing.FakeBillingManager
import com.yorvana.data.billing.RestoreResult
import com.yorvana.data.model.OdometerUnit
import com.yorvana.data.preferences.AppPreferences
import com.yorvana.data.preferences.AppPreferencesStore
import com.yorvana.data.preferences.DebugBillingOverrideMode
import com.yorvana.data.storage.VaultStorage
import com.yorvana.domain.AppGate
import com.yorvana.ui.util.ViewModelDependencies
import com.yorvana.ui.util.testInstance
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.unmockkStatic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
class SettingsViewModelTest {
    private lateinit var preferences: AppPreferencesStore
    private lateinit var vaultStorage: VaultStorage
    private lateinit var app: YorvanaApplication
    private lateinit var viewModel: SettingsViewModel
    private lateinit var appGate: AppGate
    private lateinit var isReadOnlyFlow: MutableStateFlow<Boolean>
    private lateinit var hasPendingPurchaseFlow: MutableStateFlow<Boolean>
    private lateinit var fakeBilling: FakeBillingManager
    private lateinit var debugBillingOverride: DebugBillingOverride
    private val testDispatcher = UnconfinedTestDispatcher()

    @Before
    fun setUp() {
        mockkStatic(Log::class)
        every { Log.e(any<String>(), any<String>(), any<Throwable>()) } returns 0
        every { Log.w(any<String>(), any<String>()) } returns 0

        Dispatchers.setMain(testDispatcher)
        preferences = mockk(relaxed = true)
        vaultStorage = mockk(relaxed = true)
        app = mockk(relaxed = true)
        appGate = mockk(relaxed = true)
        fakeBilling = FakeBillingManager()
        debugBillingOverride = mockk(relaxed = true)
        isReadOnlyFlow = MutableStateFlow(false)


Output:
Chunk ID: 460314
Wall time: 0.3911 seconds
Process exited with code 0
Original token count: 2106
Output:
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenIT.kt:198:                .getString(com.yorvana.R.string.record_performer_self)
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt:46:        every { context.getString(1) } returns "Mocked Label"
app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenTest.kt:200:        val dateLabel = composeTestRule.activity.getString(com.yorvana.R.string.record_date)
app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenTest.kt:201:        val odometerLabel = composeTestRule.activity.getString(com.yorvana.R.string.record_odometer)
app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenTest.kt:202:        val performerLabel = composeTestRule.activity.getString(com.yorvana.R.string.record_performer)
app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenTest.kt:208:                composeTestRule.activity.getString(
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenTest.kt:54:        val moreLabel = composeTestRule.activity.getString(R.string.action_more)
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenTest.kt:58:        val editLabel = composeTestRule.activity.getString(R.string.vehicle_edit)
app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt:48:    private fun getString(
app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt:50:    ): String = ApplicationProvider.getApplicationContext<Context>().getString(resId)
app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt:81:        composeTestRule.onNodeWithText(getString(R.string.consent_dialog_enable)).performClick()
app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt:93:        composeTestRule.onNodeWithText(getString(R.string.consent_dialog_not_now)).performClick()
app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt:113:        composeTestRule.onNodeWithText(getString(R.string.settings_privacy_description)).assertIsDisplayed()
app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt:122:        composeTestRule.onNodeWithText(getString(R.string.action_show_less)).performClick()
app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt:123:        composeTestRule.onNodeWithText(getString(R.string.settings_privacy_description)).assertDoesNotExist()
app/src/test/java/com/yorvana/ui/records/RecordDetailScreenIT.kt:104:                .getString(com.yorvana.R.string.record_performer_self)
app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleScreenTest.kt:66:        val nicknameLabel = composeTestRule.activity.getString(com.yorvana.R.string.vehicle_nickname)
app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleScreenTest.kt:67:        val expectedAnnouncement = composeTestRule.activity.getString(com.yorvana.R.string.common_required_suffix, nicknameLabel)
app/src/test/java/com/yorvana/ui/navigation/SetupScreenTest.kt:28:        val welcomeText = context.getString(com.yorvana.R.string.setup_title)
app/src/test/java/com/yorvana/ui/navigation/SetupScreenTest.kt:29:        val chooseFolderText = context.getString(com.yorvana.R.string.setup_choose_folder)
app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleViewModelTest.kt:55:        every { application.getString(any()) } returns "Premium required for additional vehicles"
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:233:                assertThat(fallback.body).contains(app.getString(R.string.report_bug_email_device_info))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:234:                assertThat(fallback.body).contains(app.getString(R.string.report_bug_app_version))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:235:                assertThat(fallback.body).contains(app.getString(R.string.report_bug_android_version))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:236:                assertThat(fallback.body).contains(app.getString(R.string.report_bug_locale))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:237:                assertThat(fallback.body).contains(app.getString(R.string.report_bug_device_model))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:238:                assertThat(fallback.body).contains(app.getString(R.string.report_bug_vault_configured))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:262:                assertThat(fallback.body).doesNotContain(app.getString(R.string.report_bug_email_device_info))
app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:308:                assertThat(fallback.body).contains(app.getString(R.string.report_bug_email_contact_label))
app/src/test/java/com/yorvana/ui/settings/SettingsScreenshotTest.kt:121:        composeTestRule.onNodeWithText(context.getString(R.string.settings_report_bug))
app/src/test/java/com/yorvana/ui/settings/SettingsScreenshotTest.kt:131:        composeTestRule.onNodeWithText(context.getString(R.string.settings_force_test_crash))
app/src/test/java/com/yorvana/ui/settings/SettingsScreenshotTest.kt:141:        composeTestRule.onNodeWithText(context.getString(R.string.settings_force_test_crash))
app/src/test/java/com/yorvana/ui/settings/CategoriesScreenTest.kt:45:        val categoryLabel = composeTestRule.activity.getString(com.yorvana.R.string.category_custom_label)
app/src/test/java/com/yorvana/ui/settings/CategoriesScreenTest.kt:46:        val expectedAnnouncement = composeTestRule.activity.getString(com.yorvana.R.string.common_required_suffix, categoryLabel)
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:82:        every { app.getString(R.string.settings_debug_purchase_success) } returns "Simulated purchase success"
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:83:        every { app.getString(R.string.settings_debug_billing_reset) } returns "Billing state reset"
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:84:        every { app.getString(R.string.report_bug_support_email) } returns "yorvana.app@gmail.com"
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:245:            every { app.getString(R.string.restore_success) } returns "Purchase restored successfully"
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:261:            every { app.getString(R.string.restore_none_found) } returns "No previous purchases found"
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:277:            every { app.getString(R.string.restore_error_billing_unavailable) } returns "Restore failed: billing unavailable"
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:293:            every { app.getString(R.string.restore_error_code, 3) } returns "Restore failed (code 3)"
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:309:            every { app.getString(any<Int>()) } returns ""
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:322:            every { app.getString(any<Int>()) } returns ""
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:382:            every { app.getString(R.string.billing_error, "SDK error") } returns "Purchase failed: SDK error"
app/src/test/java/com/yorvana/ui/reportbug/ReportBugScreenTest.kt:50:        composeTestRule.onNodeWithText(context.getString(R.string.report_bug_title)).assertExists()
app/src/test/java/com/yorvana/ui/reportbug/ReportBugScreenTest.kt:62:        composeTestRule.onNodeWithText(context.getString(R.string.report_bug_sentry_opt_out_disclosure)).assertExists()
app/src/test/java/com/yorvana/ui/reportbug/ReportBugScreenTest.kt:69:        composeTestRule.onNodeWithText(context.getString(R.string.report_bug_sentry_opt_out_disclosure)).assertDoesNotExist()
app/src/test/java/com/yorvana/ui/reportbug/ReportBugScreenTest.kt:149:        composeTestRule.onNodeWithText(context.getString(R.string.report_bug_contact_email_invalid)).assertExists()
app/src/test/java/com/yorvana/ui/components/RequiredLabelTest.kt:67:        val label = composeTestRule.activity.getString(com.yorvana.R.string.vehicle_nickname)
app/src/test/java/com/yorvana/ui/components/RequiredLabelTest.kt:68:        val expectedAnnouncement = composeTestRule.activity.getString(com.yorvana.R.string.common_required_suffix, label)


sed -n '70,90p' app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt


Output:
Chunk ID: 6bfbba
Wall time: 0.1148 seconds
Process exited with code 0
Original token count: 328
Output:
        isReadOnlyFlow = MutableStateFlow(false)
        hasPendingPurchaseFlow = MutableStateFlow(false)

        // Setup PackageManager mocks because SettingsViewModel reads the app version name on init.
        val pm = mockk<PackageManager>()
        val pi = PackageInfo().apply { versionName = "1.0.0" }
        every { app.applicationContext } returns app
        every { app.packageName } returns "com.yorvana"
        every { app.packageManager } returns pm
        every { pm.getPackageInfo("com.yorvana", 0) } returns pi

        // Mock localized strings for debug events
        every { app.getString(R.string.settings_debug_purchase_success) } returns "Simulated purchase success"
        every { app.getString(R.string.settings_debug_billing_reset) } returns "Billing state reset"
        every { app.getString(R.string.report_bug_support_email) } returns "yorvana.app@gmail.com"

        every { preferences.preferences } returns flowOf(AppPreferences(vaultUriString = "content://vault"))
        every { appGate.isReadOnly } returns isReadOnlyFlow
        every { appGate.hasPendingPurchase } returns hasPendingPurchaseFlow
        every { appGate.isPremium } returns fakeBilling.premiumState
        every { debugBillingOverride.mode } returns MutableStateFlow(DebugBillingOverrideMode.NONE)


I found the concrete edit points. I’m applying the resource cleanup and helper API change in one small patch, then I’ll rerun the focused tests plus lint/format checks.


Input:
*** Begin Patch
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/util/EmailLaunchRequest.kt
@@
 import android.content.ActivityNotFoundException
 import android.content.Context
 import android.content.Intent
+import androidx.annotation.StringRes
 import androidx.compose.material3.SnackbarHostState
 import androidx.core.net.toUri
@@
 suspend fun launchEmail(
     context: Context,
     snackbarHostState: SnackbarHostState,
-    noEmailAppMessage: String,
+    @StringRes noEmailAppMessageRes: Int,
     request: EmailLaunchRequest,
 ) {
@@
     try {
         context.startActivity(intent)
     } catch (_: ActivityNotFoundException) {
-        snackbarHostState.showSnackbar(noEmailAppMessage)
+        snackbarHostState.showSnackbar(context.getString(noEmailAppMessageRes))
     }
 }
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt
@@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
@@
     val snackbarHostState = remember { SnackbarHostState() }
     val isDark = isSystemInDarkTheme()
     val upgradeLauncher = rememberUpgradeLauncher(snackbarHostState)
     val fileManagerError = stringResource(R.string.settings_file_manager_error)
-    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)

@@
                         context = context,
                         snackbarHostState = snackbarHostState,
-                        noEmailAppMessage = noEmailAppMessage,
+                        noEmailAppMessageRes = R.string.email_no_app_found,
                         request =
                             EmailLaunchRequest(
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt
@@
     val scrollState = rememberScrollState()

     var showDeviceInfo by rememberSaveable { mutableStateOf(false) }

-    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)
-
     LaunchedEffect(Unit) {
         viewModel.effects.collect { effect ->
             when (effect) {
@@
                         context = context,
                         snackbarHostState = snackbarHostState,
-                        noEmailAppMessage = noEmailAppMessage,
+                        noEmailAppMessageRes = R.string.email_no_app_found,
                         request =
                             EmailLaunchRequest(
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt
@@
                         SettingsEffect.LaunchContactEmail(
                             recipient = app.getString(R.string.report_bug_support_email),
-                            subject = "[Yorvana] Contact",
+                            subject = app.getString(R.string.settings_contact_email_subject),
                         ),
                     )
                 }
*** Update File: /home/Messier82/projects/service-book/app/src/main/res/values/strings.xml
@@
     <string name="settings_about">About</string>
     <string name="settings_contact">Contact</string>
     <string name="settings_contact_subtitle">Email Yorvana support</string>
+    <string name="settings_contact_email_subject">[Yorvana] Contact</string>
     <string name="settings_report_bug">Report a bug or suggest a feature</string>
     <string name="settings_version">Version %s</string>
@@
     <string name="report_bug_email_contact_label">Contact email</string>
     <string name="report_bug_type_section">Type</string>
-    <string name="report_bug_no_email_app">No email app found</string>
+    <string name="email_no_app_found">No email app found</string>
     <string name="report_bug_include_device_info">Include additional app state</string>
*** Update File: /home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt
@@
         every { app.getString(R.string.settings_debug_purchase_success) } returns "Simulated purchase success"
         every { app.getString(R.string.settings_debug_billing_reset) } returns "Billing state reset"
         every { app.getString(R.string.report_bug_support_email) } returns "yorvana.app@gmail.com"
+        every { app.getString(R.string.settings_contact_email_subject) } returns "[Yorvana] Contact"
*** End Patch


Success. Updated the following files:
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/util/EmailLaunchRequest.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt
M /home/Messier82/projects/service-book/app/src/main/res/values/strings.xml
M /home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt

@@ -84,2 +84,3 @@
         every { app.getString(R.string.report_bug_support_email) } returns "yorvana.app@gmail.com"
+        every { app.getString(R.string.settings_contact_email_subject) } returns "[Yorvana] Contact"

@@ -214,3 +214,3 @@
                             recipient = app.getString(R.string.report_bug_support_email),
-                            subject = "[Yorvana] Contact",
+                            subject = app.getString(R.string.settings_contact_email_subject),
                         ),

@@ -111,2 +111,3 @@
     <string name="settings_contact_subtitle">Email Yorvana support</string>
+    <string name="settings_contact_email_subject">[Yorvana] Contact</string>
     <string name="settings_report_bug">Report a bug or suggest a feature</string>
@@ -124,3 +125,3 @@
     <string name="report_bug_type_section">Type</string>
-    <string name="report_bug_no_email_app">No email app found</string>
+    <string name="email_no_app_found">No email app found</string>
     <string name="report_bug_include_device_info">Include additional app state</string>

@@ -107,3 +107,2 @@
     val fileManagerError = stringResource(R.string.settings_file_manager_error)
-    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)

@@ -142,3 +141,3 @@
                         snackbarHostState = snackbarHostState,
-                        noEmailAppMessage = noEmailAppMessage,
+                        noEmailAppMessageRes = R.string.email_no_app_found,
                         request =

@@ -5,2 +5,3 @@
 import android.content.Intent
+import androidx.annotation.StringRes
 import androidx.compose.material3.SnackbarHostState
@@ -17,3 +18,3 @@
     snackbarHostState: SnackbarHostState,
-    noEmailAppMessage: String,
+    @StringRes noEmailAppMessageRes: Int,
     request: EmailLaunchRequest,
@@ -32,3 +33,3 @@
     } catch (_: ActivityNotFoundException) {
-        snackbarHostState.showSnackbar(noEmailAppMessage)
+        snackbarHostState.showSnackbar(context.getString(noEmailAppMessageRes))
     }

@@ -77,4 +77,2 @@

-    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)
-
     LaunchedEffect(Unit) {
@@ -88,3 +86,3 @@
                         snackbarHostState = snackbarHostState,
-                        noEmailAppMessage = noEmailAppMessage,
+                        noEmailAppMessageRes = R.string.email_no_app_found,
                         request =


rg -n "report_bug_no_email_app|email_no_app_found|settings_contact_email_subject|noEmailAppMessage" app/src


Output:
Chunk ID: 81c947
Wall time: 0.2135 seconds
Process exited with code 0
Original token count: 271
Output:
app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt:87:                        noEmailAppMessageRes = R.string.email_no_app_found,
app/src/main/java/com/yorvana/ui/util/EmailLaunchRequest.kt:19:    @StringRes noEmailAppMessageRes: Int,
app/src/main/java/com/yorvana/ui/util/EmailLaunchRequest.kt:34:        snackbarHostState.showSnackbar(context.getString(noEmailAppMessageRes))
app/src/main/res/values/strings.xml:112:    <string name="settings_contact_email_subject">[Yorvana] Contact</string>
app/src/main/res/values/strings.xml:126:    <string name="email_no_app_found">No email app found</string>
app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt:215:                            subject = app.getString(R.string.settings_contact_email_subject),
app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt:142:                        noEmailAppMessageRes = R.string.email_no_app_found,
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt:85:        every { app.getString(R.string.settings_contact_email_subject) } returns "[Yorvana] Contact"


Output:
Chunk ID: 307d52
Wall time: 0.1990 seconds
Process exited with code 0
Original token count: 1628
Output:
diff --git a/app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt b/app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt
index e60fd89..3b88241 100644
--- a/app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt
+++ b/app/src/main/java/com/yorvana/ui/reportbug/ReportBugScreen.kt
@@ -75,8 +75,6 @@ fun ReportBugScreen(

     var showDeviceInfo by rememberSaveable { mutableStateOf(false) }

-    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)
-
     LaunchedEffect(Unit) {
         viewModel.effects.collect { effect ->
             when (effect) {
@@ -86,7 +84,7 @@ fun ReportBugScreen(
                     launchEmail(
                         context = context,
                         snackbarHostState = snackbarHostState,
-                        noEmailAppMessage = noEmailAppMessage,
+                        noEmailAppMessageRes = R.string.email_no_app_found,
                         request =
                             EmailLaunchRequest(
                                 recipient = effect.recipient,
diff --git a/app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt b/app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt
index 9ae0af2..2778e2f 100644
--- a/app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/yorvana/ui/settings/SettingsScreen.kt
@@ -105,7 +105,6 @@ fun SettingsScreen(
     val isDark = isSystemInDarkTheme()
     val upgradeLauncher = rememberUpgradeLauncher(snackbarHostState)
     val fileManagerError = stringResource(R.string.settings_file_manager_error)
-    val noEmailAppMessage = stringResource(R.string.report_bug_no_email_app)

     var showOdometerDialog by remember { mutableStateOf(false) }
     var showCurrencyDialog by remember { mutableStateOf(false) }
@@ -140,7 +139,7 @@ fun SettingsScreen(
                     launchEmail(
                         context = context,
                         snackbarHostState = snackbarHostState,
-                        noEmailAppMessage = noEmailAppMessage,
+                        noEmailAppMessageRes = R.string.email_no_app_found,
                         request =
                             EmailLaunchRequest(
                                 recipient = effect.recipient,
diff --git a/app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt
index def1b00..a820e72 100644
--- a/app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/yorvana/ui/settings/SettingsViewModel.kt
@@ -212,7 +212,7 @@ class SettingsViewModel(
                     _effects.send(
                         SettingsEffect.LaunchContactEmail(
                             recipient = app.getString(R.string.report_bug_support_email),
-                            subject = "[Yorvana] Contact",
+                            subject = app.getString(R.string.settings_contact_email_subject),
                         ),
                     )
                 }
diff --git a/app/src/main/java/com/yorvana/ui/util/EmailLaunchRequest.kt b/app/src/main/java/com/yorvana/ui/util/EmailLaunchRequest.kt
index e6e50be..607baa2 100644
--- a/app/src/main/java/com/yorvana/ui/util/EmailLaunchRequest.kt
+++ b/app/src/main/java/com/yorvana/ui/util/EmailLaunchRequest.kt
@@ -3,6 +3,7 @@ package com.yorvana.ui.util
 import android.content.ActivityNotFoundException
 import android.content.Context
 import android.content.Intent
+import androidx.annotation.StringRes
 import androidx.compose.material3.SnackbarHostState
 import androidx.core.net.toUri

@@ -15,7 +16,7 @@ data class EmailLaunchRequest(
 suspend fun launchEmail(
     context: Context,
     snackbarHostState: SnackbarHostState,
-    noEmailAppMessage: String,
+    @StringRes noEmailAppMessageRes: Int,
     request: EmailLaunchRequest,
 ) {
     val intent =
@@ -30,6 +31,6 @@ suspend fun launchEmail(
     try {
         context.startActivity(intent)
     } catch (_: ActivityNotFoundException) {
-        snackbarHostState.showSnackbar(noEmailAppMessage)
+        snackbarHostState.showSnackbar(context.getString(noEmailAppMessageRes))
     }
 }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f7e30b7..158e7c8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -109,6 +109,7 @@
     <string name="settings_about">About</string>
     <string name="settings_contact">Contact</string>
     <string name="settings_contact_subtitle">Email Yorvana support</string>
+    <string name="settings_contact_email_subject">[Yorvana] Contact</string>
     <string name="settings_report_bug">Report a bug or suggest a feature</string>
     <string name="settings_version">Version %s</string>

@@ -122,7 +123,7 @@
     <string name="report_bug_contact_email_invalid">Please enter a valid email address</string>
     <string name="report_bug_email_contact_label">Contact email</string>
     <string name="report_bug_type_section">Type</string>
-    <string name="report_bug_no_email_app">No email app found</string>
+    <string name="email_no_app_found">No email app found</string>
     <string name="report_bug_include_device_info">Include additional app state</string>
     <string name="report_bug_device_info_payload">Device information</string>
     <string name="report_bug_sentry_opt_out_disclosure">Submitting will connect to our error reporting service for this report only. Basic device info is always attached.</string>
diff --git a/app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt b/app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt
index a7addf2..563b99d 100644
--- a/app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt
+++ b/app/src/test/java/com/yorvana/ui/settings/SettingsViewModelTest.kt
@@ -82,6 +82,7 @@ class SettingsViewModelTest {
         every { app.getString(R.string.settings_debug_purchase_success) } returns "Simulated purchase success"
         every { app.getString(R.string.settings_debug_billing_reset) } returns "Billing state reset"
         every { app.getString(R.string.report_bug_support_email) } returns "yorvana.app@gmail.com"
+        every { app.getString(R.string.settings_contact_email_subject) } returns "[Yorvana] Contact"

         every { preferences.preferences } returns flowOf(AppPreferences(vaultUriString = "content://vault"))
         every { appGate.isReadOnly } returns isReadOnlyFlow


The patch removes the old feature-specific resource name entirely, and the helper now resolves the fallback string from the context via a `@StringRes`. I’m running the same focused test set first, then the formatting/static checks if that stays green.


./gradlew :app:testDebugUnitTest --tests com.yorvana.ui.settings.SettingsViewModelTest --tests com.yorvana.ui.settings.SettingsScreenIT --tests com.yorvana.ui.reportbug.ReportBugViewModelTest


Output:
Chunk ID: 01a9d1
Wall time: 1.0005 seconds
Process running with session ID 34611
Original token count: 35
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off
Starting a Gradle Daemon, 1 busy Daemon could not be reused, use --status for details


Output:
Chunk ID: d49eef
Wall time: 5.0011 seconds
Process running with session ID 34611
Original token count: 0
Output:


Output:
Chunk ID: 8fd9c3
Wall time: 5.0016 seconds
Process running with session ID 34611
Original token count: 321
Output:

> Configure project :app
WARNING: The option setting 'android.testoptions.manageddevices.emulator.gpu=host' is experimental.

> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:generateDebugResources
> Task :app:packageDebugResources
> Task :app:processDebugNavigationResources
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:mapDebugSourceSetPaths
> Task :app:compileDebugNavigationResources UP-TO-DATE
> Task :app:parseDebugLocalResources
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:generateDebugRFile
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:mergeDebugResources
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:preDebugUnitTestBuild UP-TO-DATE
> Task :app:javaPreCompileDebugUnitTest UP-TO-DATE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:processDebugUnitTestManifest UP-TO-DATE
> Task :app:restoreOutputDirRoborazzi SKIPPED
> Task :app:processDebugResources
> Task :app:packageDebugUnitTestForUnitTest
> Task :app:generateDebugUnitTestConfig


Output:
Chunk ID: ce446e
Wall time: 5.0013 seconds
Process running with session ID 34611
Original token count: 0
Output:


Output:
Chunk ID: 40f33e
Wall time: 5.0013 seconds
Process running with session ID 34611
Original token count: 50
Output:
> Task :app:compileDebugKotlin
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:processDebugJavaRes
> Task :app:bundleDebugClassesToCompileJar
> Task :app:bundleDebugClassesToRuntimeJar


Output:
Chunk ID: f89cb8
Wall time: 5.0008 seconds
Process running with session ID 34611
Original token count: 1715
Output:

> Task :app:compileDebugUnitTestKotlin
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/components/RequiredLabelTest.kt:27:27 'fun <reified A : ComponentActivity> createAndroidComposeRule(effectContext: CoroutineContext = ...): AndroidComposeTestRule<ActivityScenarioRule<A>, A>' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createAndroidComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/navigation/SetupScreenTest.kt:17:27 'fun createComposeRule(effectContext: CoroutineContext = ...): ComposeContentTestRule' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenTest.kt:26:27 'fun <reified A : ComponentActivity> createAndroidComposeRule(effectContext: CoroutineContext = ...): AndroidComposeTestRule<ActivityScenarioRule<A>, A>' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createAndroidComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/records/RecordDetailScreenIT.kt:23:27 'fun createComposeRule(effectContext: CoroutineContext = ...): ComposeContentTestRule' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenIT.kt:28:27 'fun createComposeRule(effectContext: CoroutineContext = ...): ComposeContentTestRule' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenTest.kt:30:27 'fun <reified A : ComponentActivity> createAndroidComposeRule(effectContext: CoroutineContext = ...): AndroidComposeTestRule<ActivityScenarioRule<A>, A>' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createAndroidComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/reportbug/ReportBugScreenTest.kt:30:27 'fun createComposeRule(effectContext: CoroutineContext = ...): ComposeContentTestRule' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:232:39 No cast needed.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:261:39 No cast needed.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/reportbug/ReportBugViewModelTest.kt:307:39 No cast needed.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/settings/SettingsScreenshotTest.kt:24:27 'fun createComposeRule(effectContext: CoroutineContext = ...): ComposeContentTestRule' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/vehicles/AddEditVehicleScreenTest.kt:26:27 'fun <reified A : ComponentActivity> createAndroidComposeRule(effectContext: CoroutineContext = ...): AndroidComposeTestRule<ActivityScenarioRule<A>, A>' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createAndroidComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/vehicles/VehicleListConsentDialogTest.kt:34:27 'fun createComposeRule(effectContext: CoroutineContext = ...): ComposeContentTestRule' is deprecated. Use `androidx.compose.ui.test.junit4.v2.createComposeRule` instead. The v2 APIs use StandardTestDispatcher instead of UnconfinedTestDispatcher, which aligns with standard coroutine behavior by queuing tasks rather than executing them immediately. Tests relying on immediate execution may require explicit synchronization. Please refer to the migration guide for more details.

> Task :app:compileDebugUnitTestJavaWithJavac NO-SOURCE
> Task :app:processDebugUnitTestJavaRes UP-TO-DATE


Output:
Chunk ID: 4340ce
Wall time: 5.0005 seconds
Process running with session ID 34611
Original token count: 132
Output:

> Task :app:testDebugUnitTestPicked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off

Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off

OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended


Output:
Chunk ID: 5c7e99
Wall time: 5.0016 seconds
Process running with session ID 34611
Original token count: 196
Output:

> Task :app:finalizeTestRoborazziDebug SKIPPED

[Incubating] Problems report is available at: file:///home/Messier82/projects/service-book/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 50s
30 actionable tasks: 16 executed, 14 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.5.1/userguide/configuration_cache_enabling.html


The focused tests passed. I’m running the formatting and static checks next before amending and pushing the PR branch.


Output:
Chunk ID: f5392a
Wall time: 1.0010 seconds
Process running with session ID 81945
Original token count: 35
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off
Starting a Gradle Daemon, 2 busy Daemons could not be reused, use --status for details


Output:
Chunk ID: 985ecd
Wall time: 1.0016 seconds
Process running with session ID 6695
Original token count: 13
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off


Output:
Chunk ID: 9e94c7
Wall time: 5.0003 seconds
Process running with session ID 81945
Original token count: 32
Output:

> Configure project :app
WARNING: The option setting 'android.testoptions.manageddevices.emulator.gpu=host' is experimental.


Output:
Chunk ID: c92a20
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 216
Output:

> Configure project :app
WARNING: The option setting 'android.testoptions.manageddevices.emulator.gpu=host' is experimental.

> Task :app:detekt

[Incubating] Problems report is available at: file:///home/Messier82/projects/service-book/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.5.1/userguide/configuration_cache_enabling.html


Output:
Chunk ID: 13708c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 225
Output:

> Task :app:loadKtlintReporters UP-TO-DATE
> Task :app:runKtlintCheckOverKotlinScripts UP-TO-DATE
> Task :app:ktlintKotlinScriptCheck UP-TO-DATE
> Task :app:ktlintCheck UP-TO-DATE

[Incubating] Problems report is available at: file:///home/Messier82/projects/service-book/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 10s
3 actionable tasks: 3 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.5.1/userguide/configuration_cache_enabling.html


The checks are clean. I’m going to add a small unit test around the shared email helper as well, since the review called out the fallback path and this helper is now the right seam to exercise it.


Input:
*** Begin Patch
*** Add File: /home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/util/EmailLaunchRequestTest.kt
+package com.yorvana.ui.util
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import androidx.compose.material3.SnackbarHostState
+import androidx.core.net.toUri
+import com.google.common.truth.Truth.assertThat
+import com.yorvana.R
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import io.mockk.slot
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class EmailLaunchRequestTest {
+    @Test
+    fun `launchEmail starts mailto email intent`() =
+        runTest {
+            val context = mockk<Context>()
+            val intentSlot = slot<Intent>()
+            every { context.startActivity(capture(intentSlot)) } just runs
+
+            launchEmail(
+                context = context,
+                snackbarHostState = SnackbarHostState(),
+                noEmailAppMessageRes = R.string.email_no_app_found,
+                request =
+                    EmailLaunchRequest(
+                        recipient = "support@example.com",
+                        subject = "Subject",
+                        body = "Body",
+                    ),
+            )
+
+            val intent = intentSlot.captured
+            assertThat(intent.action).isEqualTo(Intent.ACTION_SENDTO)
+            assertThat(intent.data).isEqualTo("mailto:".toUri())
+            assertThat(intent.getStringArrayExtra(Intent.EXTRA_EMAIL)).asList().containsExactly("support@example.com")
+            assertThat(intent.getStringExtra(Intent.EXTRA_SUBJECT)).isEqualTo("Subject")
+            assertThat(intent.getStringExtra(Intent.EXTRA_TEXT)).isEqualTo("Body")
+        }
+
+    @Test
+    fun `launchEmail shows snackbar when no email app is available`() =
+        runTest {
+            val context = mockk<Context>()
+            val snackbarHostState = SnackbarHostState()
+            every { context.startActivity(any()) } throws ActivityNotFoundException()
+            every { context.getString(R.string.email_no_app_found) } returns "No email app found"
+
+            val launchJob =
+                launch {
+                    launchEmail(
+                        context = context,
+                        snackbarHostState = snackbarHostState,
+                        noEmailAppMessageRes = R.string.email_no_app_found,
+                        request =
+                            EmailLaunchRequest(
+                                recipient = "support@example.com",
+                                subject = "Subject",
+                            ),
+                    )
+                }
+            runCurrent()
+
+            assertThat(snackbarHostState.currentSnackbarData?.visuals?.message).isEqualTo("No email app found")
+            snackbarHostState.currentSnackbarData?.dismiss()
+            launchJob.join()
+        }
+}
*** End Patch


Success. Updated the following files:
A /home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/util/EmailLaunchRequestTest.kt

package com.yorvana.ui.util

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.compose.material3.SnackbarHostState
import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import com.yorvana.R
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.slot
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test

class EmailLaunchRequestTest {
    @Test
    fun `launchEmail starts mailto email intent`() =
        runTest {
            val context = mockk<Context>()
            val intentSlot = slot<Intent>()
            every { context.startActivity(capture(intentSlot)) } just runs

            launchEmail(
                context = context,
                snackbarHostState = SnackbarHostState(),
                noEmailAppMessageRes = R.string.email_no_app_found,
                request =
                    EmailLaunchRequest(
                        recipient = "support@example.com",
                        subject = "Subject",
                        body = "Body",
                    ),
            )

            val intent = intentSlot.captured
            assertThat(intent.action).isEqualTo(Intent.ACTION_SENDTO)
            assertThat(intent.data).isEqualTo("mailto:".toUri())
            assertThat(intent.getStringArrayExtra(Intent.EXTRA_EMAIL)).asList().containsExactly("support@example.com")
            assertThat(intent.getStringExtra(Intent.EXTRA_SUBJECT)).isEqualTo("Subject")
            assertThat(intent.getStringExtra(Intent.EXTRA_TEXT)).isEqualTo("Body")
        }

    @Test
    fun `launchEmail shows snackbar when no email app is available`() =
        runTest {
            val context = mockk<Context>()
            val snackbarHostState = SnackbarHostState()
            every { context.startActivity(any()) } throws ActivityNotFoundException()
            every { context.getString(R.string.email_no_app_found) } returns "No email app found"

            val launchJob =
                launch {
                    launchEmail(
                        context = context,
                        snackbarHostState = snackbarHostState,
                        noEmailAppMessageRes = R.string.email_no_app_found,
                        request =
                            EmailLaunchRequest(
                                recipient = "support@example.com",
                                subject = "Subject",
                            ),
                    )
                }
            runCurrent()

            assertThat(snackbarHostState.currentSnackbarData?.visuals?.message).isEqualTo("No email app found")
            snackbarHostState.currentSnackbarData?.dismiss()
            launchJob.join()
        }
}


Output: {"output":"Success. Updated the following files:\nA /home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/util/EmailLaunchRequestTest.kt\n","metadata":{"exit_code":0,"duration_seconds":0.8}}


./gradlew :app:testDebugUnitTest --tests com.yorvana.ui.util.EmailLaunchRequestTest --tests com.yorvana.ui.settings.SettingsViewModelTest --tests com.yorvana.ui.settings.SettingsScreenIT --tests com.yorvana.ui.reportbug.ReportBugViewModelTest


Output:
Chunk ID: 55b364
Wall time: 1.0010 seconds
Process running with session ID 76486
Original token count: 14
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off


Output:
Chunk ID: 06624d
Wall time: 5.0011 seconds
Process running with session ID 76486
Original token count: 408
Output:
> Configure project :app
WARNING: The option setting 'android.testoptions.manageddevices.emulator.gpu=host' is experimental.

> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:packageDebugResources UP-TO-DATE
> Task :app:processDebugNavigationResources UP-TO-DATE
> Task :app:parseDebugLocalResources UP-TO-DATE
> Task :app:generateDebugRFile UP-TO-DATE
> Task :app:compileDebugKotlin UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:mapDebugSourceSetPaths UP-TO-DATE
> Task :app:compileDebugNavigationResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:bundleDebugClassesToRuntimeJar UP-TO-DATE
> Task :app:bundleDebugClassesToCompileJar UP-TO-DATE
> Task :app:preDebugUnitTestBuild UP-TO-DATE
> Task :app:javaPreCompileDebugUnitTest UP-TO-DATE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:packageDebugUnitTestForUnitTest UP-TO-DATE
> Task :app:processDebugUnitTestManifest UP-TO-DATE
> Task :app:generateDebugUnitTestConfig UP-TO-DATE
> Task :app:processDebugJavaRes UP-TO-DATE
> Task :app:restoreOutputDirRoborazzi SKIPPED


Output:
Chunk ID: 32efe0
Wall time: 5.0013 seconds
Process running with session ID 76486
Original token count: 145
Output:

> Task :app:compileDebugUnitTestKotlin
w: file:///home/Messier82/projects/service-book/app/src/test/java/com/yorvana/ui/util/EmailLaunchRequestTest.kt:69:13 This declaration needs opt-in. Its usage should be marked with '@kotlinx.coroutines.ExperimentalCoroutinesApi' or '@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)'

> Task :app:compileDebugUnitTestJavaWithJavac NO-SOURCE
> Task :app:processDebugUnitTestJavaRes UP-TO-DATE

> Task :app:testDebugUnitTest
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off


Output:
Chunk ID: 5b783c
Wall time: 5.0011 seconds
Process running with session ID 76486
Original token count: 98
Output:

OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended


Output:
Chunk ID: 7fcdcb
Wall time: 2.4732 seconds
Process exited with code 1
Original token count: 375
Output:
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

> Task :app:testDebugUnitTest

EmailLaunchRequestTest > launchEmail starts mailto email intent FAILED
    java.lang.RuntimeException at EmailLaunchRequestTest.kt:23

EmailLaunchRequestTest > launchEmail shows snackbar when no email app is available FAILED
    java.lang.RuntimeException at EmailLaunchRequestTest.kt:50

43 tests completed, 2 failed

> Task :app:testDebugUnitTest FAILED
> Task :app:finalizeTestRoborazziDebug SKIPPED

[Incubating] Problems report is available at: file:///home/Messier82/projects/service-book/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:testDebugUnitTest'.
> There were failing tests. See the results at: file:///home/Messier82/projects/service-book/app/build/test-results/testDebugUnitTest/

* Try:
> Run with --scan to get full insights from a Build Scan (powered by Develocity).

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD FAILED in 26s
30 actionable tasks: 2 executed, 28 up-to-date


The new helper test exposed a test-environment issue rather than a product failure. I’m reading the failure details now; if the test needs Android resource setup that this suite doesn’t provide, I’ll keep the production cleanup and drop the brittle test.


sed -n '1,160p' app/build/test-results/testDebugUnitTest/TEST-com.yorvana.ui.util.EmailLaunchRequestTest.xml