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