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" />
+
= 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/relays/AmberRelayStat.kt b/app/src/main/java/com/greenart7c3/nostrsigner/relays/AmberRelayStat.kt
index ef9d0b03..4e323106 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/relays/AmberRelayStat.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/relays/AmberRelayStat.kt
@@ -20,6 +20,7 @@ import com.greenart7c3.nostrsigner.MainActivity
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.service.KillSwitchReceiver
import com.greenart7c3.nostrsigner.service.ReconnectReceiver
+import com.greenart7c3.nostrsigner.service.StopServiceReceiver
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
@@ -121,6 +122,14 @@ class AmberRelayStats(
PendingIntent.FLAG_MUTABLE,
)
+ val stopServiceIntent = Intent(appContext, StopServiceReceiver::class.java)
+ val stopServicePendingIntent = PendingIntent.getBroadcast(
+ appContext,
+ 0,
+ stopServiceIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
+ )
+
return NotificationCompat.Builder(appContext, channel.id)
.setContentTitle(appContext.getString(R.string.service))
.setContentText(appContext.getString(R.string.amber_is_running_in_background))
@@ -130,6 +139,11 @@ class AmberRelayStats(
.setGroup(group.id)
.setGroupSummary(true)
.setContentIntent(contentPendingIntent)
+ .addAction(
+ R.drawable.ic_notification,
+ appContext.getString(R.string.stop_service),
+ stopServicePendingIntent,
+ )
.build()
}
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/service/StopServiceReceiver.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/StopServiceReceiver.kt
new file mode 100644
index 00000000..cd39e3e0
--- /dev/null
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/StopServiceReceiver.kt
@@ -0,0 +1,34 @@
+package com.greenart7c3.nostrsigner.service
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Process
+import android.util.Log
+import androidx.core.app.NotificationManagerCompat
+import com.greenart7c3.nostrsigner.Amber
+import kotlin.system.exitProcess
+
+class StopServiceReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(Amber.TAG, "StopServiceReceiver: stopping service and killing process")
+
+ try {
+ Amber.instance.client.disconnect()
+ } catch (e: Exception) {
+ Log.e(Amber.TAG, "Failed to disconnect client on stop", e)
+ }
+
+ try {
+ context.stopService(Intent(context, ConnectivityService::class.java))
+ } catch (e: Exception) {
+ Log.e(Amber.TAG, "Failed to stop ConnectivityService", e)
+ }
+
+ val notificationManager = NotificationManagerCompat.from(context)
+ notificationManager.cancelAll()
+
+ Process.killProcess(Process.myPid())
+ exitProcess(0)
+ }
+}
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 83925fb7..1ce39f93 100644
--- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt
+++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt
@@ -16,6 +16,7 @@ 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
@@ -197,6 +198,20 @@ fun SettingsScreen(
},
)
}
+
+ Box(
+ Modifier
+ .padding(vertical = 8.dp),
+ ) {
+ IconRow(
+ title = stringResource(R.string.service_settings),
+ icon = Icons.Default.PowerSettingsNew,
+ tint = MaterialTheme.colorScheme.onBackground,
+ onClick = {
+ onNav(Route.ServiceSettings.route)
+ },
+ )
+ }
}
Box(
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 7a74581c..3f6943b1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -615,6 +615,10 @@
Download failed. Tap to retry.
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
Every app start
Daily