From 50e4b4db38217f9534421ff58502ef4cb17038e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 11:12:17 +0000 Subject: [PATCH 1/3] Add option to disable service start on boot Introduce a new "Start service on boot" toggle in the main settings screen (online flavor only). When disabled, BootReceiver skips Amber.startService() for ACTION_BOOT_COMPLETED so the background service does not launch until the user opens the app. Defaults to true to preserve existing behavior. ACTION_MY_PACKAGE_REPLACED still restarts the service after app updates. https://claude.ai/code/session_01N7zDP1jMW4qVeagxuyeacN --- .../nostrsigner/LocalPreferences.kt | 12 +++++++ .../nostrsigner/models/AmberSettings.kt | 1 + .../nostrsigner/service/BootReceiver.kt | 6 ++++ .../nostrsigner/ui/SettingsScreen.kt | 36 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 57 insertions(+) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt index ddec4b1c..9d017df9 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt @@ -60,6 +60,7 @@ private enum class SettingsKeys(val key: String) { AUTO_CHECK_UPDATES("auto_check_updates"), UPDATE_CHECK_FREQUENCY("update_check_frequency"), LAST_UPDATE_CHECK_TIME("last_update_check_time"), + START_SERVICE_ON_BOOT("start_service_on_boot"), WEBDAV_URL("webdav_url"), WEBDAV_USERNAME("webdav_username"), WEBDAV_PASSWORD_ENCRYPTED("webdav_password_encrypted"), @@ -139,6 +140,7 @@ object LocalPreferences { putBoolean(SettingsKeys.KILL_SWITCH.key, settings.killSwitch.value) putString(SettingsKeys.LANGUAGE_PREFS.key, settings.language) putStringSet(SettingsKeys.AUTH_WHITELIST.key, settings.authWhitelist.toSet()) + putBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, settings.startServiceOnBoot) } } } @@ -254,6 +256,7 @@ object LocalPreferences { } catch (_: IllegalArgumentException) { UpdateCheckFrequency.DAILY }, + startServiceOnBoot = getBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, true), ) } } @@ -503,6 +506,15 @@ object LocalPreferences { Amber.instance.settings = loadSettingsFromEncryptedStorage() } + fun updateStartServiceOnBoot(context: Context, enabled: Boolean) { + sharedPrefs(context).edit { + apply { + putBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, enabled) + } + } + Amber.instance.settings = loadSettingsFromEncryptedStorage() + } + fun updateUpdateCheckFrequency(context: Context, frequency: UpdateCheckFrequency) { sharedPrefs(context).edit { apply { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt index 241553b0..b3c1b661 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt @@ -32,6 +32,7 @@ data class AmberSettings( val authWhitelist: List = emptyList(), val autoCheckUpdates: Boolean = true, val updateCheckFrequency: UpdateCheckFrequency = UpdateCheckFrequency.DAILY, + val startServiceOnBoot: Boolean = true, ) { val useProxy: Boolean get() = torMode != TorMode.DISABLED } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/BootReceiver.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/BootReceiver.kt index 78d1e1df..94f8b5e3 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BootReceiver.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BootReceiver.kt @@ -21,6 +21,12 @@ class BootReceiver : BroadcastReceiver() { Log.d(Amber.TAG, "Received ${intent.action}") Amber.instance.applicationIOScope.launch { Amber.instance.isStartingAppState.first { !it } + if (intent.action == Intent.ACTION_BOOT_COMPLETED && + !Amber.instance.settings.startServiceOnBoot + ) { + Log.d(Amber.TAG, "Skipping service start on boot (disabled in settings)") + return@launch + } Amber.instance.startService() } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt index 83925fb7..a94fa7fb 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt @@ -1,6 +1,7 @@ package com.greenart7c3.nostrsigner.ui import android.util.Log +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -20,6 +21,7 @@ import androidx.compose.material.icons.filled.Security import androidx.compose.material.icons.filled.SystemUpdate import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -81,6 +83,7 @@ fun SettingsScreen( val context = LocalContext.current var torMode by remember { mutableStateOf(Amber.instance.settings.torMode) } var disconnectTorDialog by remember { mutableStateOf(false) } + var startServiceOnBoot by remember { mutableStateOf(Amber.instance.settings.startServiceOnBoot) } val scope = rememberCoroutineScope() var isLoading by remember { mutableStateOf(false) } var sizeInMBFormatted by remember { mutableStateOf("") } @@ -197,6 +200,39 @@ fun SettingsScreen( }, ) } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .clickable { + val newValue = !startServiceOnBoot + startServiceOnBoot = newValue + scope.launch(Dispatchers.IO) { + LocalPreferences.updateStartServiceOnBoot(context, newValue) + } + }, + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = stringResource(R.string.start_service_on_boot)) + Text( + text = stringResource(R.string.start_service_on_boot_description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + ) + } + Switch( + checked = startServiceOnBoot, + onCheckedChange = { enabled -> + startServiceOnBoot = enabled + scope.launch(Dispatchers.IO) { + LocalPreferences.updateStartServiceOnBoot(context, enabled) + } + }, + ) + } } Box( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a74581c..6744cb03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -615,6 +615,8 @@ Download failed. Tap to retry. Download and install update Automatically check for updates + Start service on boot + When enabled, Amber will start its background service automatically after the device boots so remote login and NIP-46 requests keep working. Check frequency Every app start Daily From 4d7a229be5d011b0de3e81654995bb3b87419230 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 13:15:44 +0000 Subject: [PATCH 2/3] Move service settings toggle to dedicated screen Replace the inline Row+Switch in the main Settings screen with an IconRow entry that navigates to a new ServiceSettingsScreen, matching the pattern used by other settings sections (Security, Update, etc.). https://claude.ai/code/session_01N7zDP1jMW4qVeagxuyeacN --- .../greenart7c3/nostrsigner/ui/MainScreen.kt | 16 +++++ .../nostrsigner/ui/ServiceSettingsScreen.kt | 70 +++++++++++++++++++ .../nostrsigner/ui/SettingsScreen.kt | 41 +++-------- .../nostrsigner/ui/navigation/Route.kt | 7 ++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/ServiceSettingsScreen.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index 53054309..724089bc 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -1018,6 +1018,22 @@ fun MainScreen( ) }, ) + + composable( + Route.ServiceSettings.route, + content = { + val scrollState = rememberScrollState() + ServiceSettingsScreen( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScrollbar(scrollState) + .verticalScroll(scrollState) + .padding(horizontal = verticalPadding) + .padding(top = verticalPadding * 1.5f), + ) + }, + ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/ServiceSettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/ServiceSettingsScreen.kt new file mode 100644 index 00000000..b5c1ae84 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/ServiceSettingsScreen.kt @@ -0,0 +1,70 @@ +package com.greenart7c3.nostrsigner.ui + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.greenart7c3.nostrsigner.Amber +import com.greenart7c3.nostrsigner.LocalPreferences +import com.greenart7c3.nostrsigner.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun ServiceSettingsScreen(modifier: Modifier = Modifier) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + var startServiceOnBoot by remember { mutableStateOf(Amber.instance.settings.startServiceOnBoot) } + + Column(modifier = modifier) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { + val newValue = !startServiceOnBoot + startServiceOnBoot = newValue + scope.launch(Dispatchers.IO) { + LocalPreferences.updateStartServiceOnBoot(context, newValue) + } + }, + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = stringResource(R.string.start_service_on_boot)) + Text( + text = stringResource(R.string.start_service_on_boot_description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + ) + } + Switch( + checked = startServiceOnBoot, + onCheckedChange = { enabled -> + startServiceOnBoot = enabled + scope.launch(Dispatchers.IO) { + LocalPreferences.updateStartServiceOnBoot(context, enabled) + } + }, + ) + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt index a94fa7fb..1ce39f93 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt @@ -1,7 +1,6 @@ package com.greenart7c3.nostrsigner.ui import android.util.Log -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,11 +16,11 @@ import androidx.compose.material.icons.filled.Feedback import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.PowerSettingsNew import androidx.compose.material.icons.filled.Security import androidx.compose.material.icons.filled.SystemUpdate import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -83,7 +82,6 @@ fun SettingsScreen( val context = LocalContext.current var torMode by remember { mutableStateOf(Amber.instance.settings.torMode) } var disconnectTorDialog by remember { mutableStateOf(false) } - var startServiceOnBoot by remember { mutableStateOf(Amber.instance.settings.startServiceOnBoot) } val scope = rememberCoroutineScope() var isLoading by remember { mutableStateOf(false) } var sizeInMBFormatted by remember { mutableStateOf("") } @@ -201,35 +199,16 @@ fun SettingsScreen( ) } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - .clickable { - val newValue = !startServiceOnBoot - startServiceOnBoot = newValue - scope.launch(Dispatchers.IO) { - LocalPreferences.updateStartServiceOnBoot(context, newValue) - } - }, + Box( + Modifier + .padding(vertical = 8.dp), ) { - Column(modifier = Modifier.weight(1f)) { - Text(text = stringResource(R.string.start_service_on_boot)) - Text( - text = stringResource(R.string.start_service_on_boot_description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - ) - } - Switch( - checked = startServiceOnBoot, - onCheckedChange = { enabled -> - startServiceOnBoot = enabled - scope.launch(Dispatchers.IO) { - LocalPreferences.updateStartServiceOnBoot(context, enabled) - } + IconRow( + title = stringResource(R.string.service_settings), + icon = Icons.Default.PowerSettingsNew, + tint = MaterialTheme.colorScheme.onBackground, + onClick = { + onNav(Route.ServiceSettings.route) }, ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt index 6eafb33b..ea78a848 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt @@ -214,6 +214,12 @@ sealed class Route( route = "CloudBackup", icon = R.drawable.settings, ) + + data object ServiceSettings : Route( + title = Amber.instance.getString(R.string.service_settings), + route = "ServiceSettings", + icon = R.drawable.settings, + ) } val routes = listOf( @@ -248,4 +254,5 @@ val routes = listOf( Route.CrashReport, Route.UpdateSettings, Route.CloudBackup, + Route.ServiceSettings, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6744cb03..9b0c7028 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -615,6 +615,7 @@ Download failed. Tap to retry. Download and install update Automatically check for updates + Service settings Start service on boot When enabled, Amber will start its background service automatically after the device boots so remote login and NIP-46 requests keep working. Check frequency From 1b166b06adc09e0c4271514096167b4a70415849 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 13:20:50 +0000 Subject: [PATCH 3/3] Add Stop service action to foreground notification Add a StopServiceReceiver that disconnects the relay client, stops the ConnectivityService, cancels notifications, and kills the app process. Surface it as a "Stop service" action on the foreground service notification so users can terminate the background service without force-stopping through system settings. https://claude.ai/code/session_01N7zDP1jMW4qVeagxuyeacN --- app/src/main/AndroidManifest.xml | 4 +++ .../nostrsigner/relays/AmberRelayStat.kt | 14 ++++++++ .../service/StopServiceReceiver.kt | 34 +++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 53 insertions(+) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/service/StopServiceReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7600e0f5..f9de2ed5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,10 @@ android:name=".service.ReconnectReceiver" android:enabled="true" android:exported="false" /> + Download and install update Automatically check for updates Service settings + Stop service Start service on boot When enabled, Amber will start its background service automatically after the device boots so remote login and NIP-46 requests keep working. Check frequency