Merge pull request #393 from greenart7c3/claude/disable-service-boot-startup-fPFHI

Add service settings and stop service functionality
This commit is contained in:
greenart7c3
2026-04-20 10:29:17 -03:00
committed by GitHub
11 changed files with 183 additions and 0 deletions
+4
View File
@@ -39,6 +39,10 @@
android:name=".service.ReconnectReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".service.StopServiceReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".service.BootReceiver"
android:enabled="true"
@@ -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 {
@@ -32,6 +32,7 @@ data class AmberSettings(
val authWhitelist: List<String> = emptyList(),
val autoCheckUpdates: Boolean = true,
val updateCheckFrequency: UpdateCheckFrequency = UpdateCheckFrequency.DAILY,
val startServiceOnBoot: Boolean = true,
) {
val useProxy: Boolean get() = torMode != TorMode.DISABLED
}
@@ -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()
}
@@ -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()
}
}
@@ -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)
}
}
@@ -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),
)
},
)
}
}
@@ -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)
}
},
)
}
}
}
@@ -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(
@@ -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,
)
+4
View File
@@ -615,6 +615,10 @@
<string name="update_download_failed">Download failed. Tap to retry.</string>
<string name="download_and_install_update">Download and install update</string>
<string name="auto_check_updates">Automatically check for updates</string>
<string name="service_settings">Service settings</string>
<string name="stop_service">Stop service</string>
<string name="start_service_on_boot">Start service on boot</string>
<string name="start_service_on_boot_description">When enabled, Amber will start its background service automatically after the device boots so remote login and NIP-46 requests keep working.</string>
<string name="update_check_frequency">Check frequency</string>
<string name="update_frequency_on_startup">Every app start</string>
<string name="update_frequency_daily">Daily</string>