diff --git a/phoenix-android/build.gradle.kts b/phoenix-android/build.gradle.kts
index b3726174c..4f5436433 100644
--- a/phoenix-android/build.gradle.kts
+++ b/phoenix-android/build.gradle.kts
@@ -55,6 +55,16 @@ android {
}
}
+ flavorDimensions += "main"
+ productFlavors {
+ create("google") {
+ versionNameSuffix = "-google"
+ }
+ create("foss") {
+ versionNameSuffix = "-foss"
+ }
+ }
+
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
@@ -134,9 +144,9 @@ dependencies {
implementation("org.slf4j:slf4j-api:${Versions.slf4j}")
implementation("com.github.tony19:logback-android:${Versions.Android.logback}")
- // firebase cloud messaging
- implementation("com.google.firebase:firebase-messaging:${Versions.Android.fcm}")
- implementation("com.google.android.gms:play-services-base:18.5.0")
+ // firebase cloud messaging -- only for the google flavor
+ "googleImplementation"("com.google.firebase:firebase-messaging:${Versions.Android.fcm}")
+ "googleImplementation"("com.google.android.gms:play-services-base:18.5.0")
implementation("com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava")
diff --git a/phoenix-android/src/foss/AndroidManifest.xml b/phoenix-android/src/foss/AndroidManifest.xml
new file mode 100644
index 000000000..f76918f51
--- /dev/null
+++ b/phoenix-android/src/foss/AndroidManifest.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/MainActivityFoss.kt b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/MainActivityFoss.kt
new file mode 100644
index 000000000..5f2ed8292
--- /dev/null
+++ b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/MainActivityFoss.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android
+
+import android.content.Context
+import android.content.Intent
+import fr.acinq.phoenix.android.services.HeadlessActions
+import fr.acinq.phoenix.android.services.NodeService
+import fr.acinq.phoenix.android.services.NodeServiceFoss
+
+class MainActivityFoss : MainActivity() {
+
+ override fun bindService() {
+ Intent(this, NodeServiceFoss::class.java).let { intent ->
+ applicationContext.bindService(intent, appViewModel.serviceConnection, Context.BIND_AUTO_CREATE or Context.BIND_ADJUST_WITH_ACTIVITY)
+ }
+ }
+
+ fun headlessServiceAction(action: HeadlessActions) {
+ val intent = Intent(applicationContext, NodeServiceFoss::class.java).apply {
+ this.action = action.name
+ putExtra(NodeService.EXTRA_ORIGIN, NodeService.ORIGIN_HEADLESS)
+ }
+ applicationContext.startForegroundService(intent)
+ }
+}
\ No newline at end of file
diff --git a/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/PhoenixApplicationFoss.kt b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/PhoenixApplicationFoss.kt
new file mode 100644
index 000000000..e331ad444
--- /dev/null
+++ b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/PhoenixApplicationFoss.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android
+
+import android.content.Intent
+import fr.acinq.phoenix.android.services.NodeService
+import fr.acinq.phoenix.android.services.NodeServiceFoss
+
+
+class PhoenixApplicationFoss : PhoenixApplication() {
+
+ override val mainActivityClass: Class get() = MainActivityFoss::class.java
+ override val nodeServiceClass: Class get() = NodeServiceFoss::class.java
+
+}
diff --git a/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/home/BackgroundRestrictionBadgeFoss.kt b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/home/BackgroundRestrictionBadgeFoss.kt
new file mode 100644
index 000000000..9f6f29754
--- /dev/null
+++ b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/home/BackgroundRestrictionBadgeFoss.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2024 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android.home
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.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.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import fr.acinq.phoenix.android.AppViewModel
+import fr.acinq.phoenix.android.R
+import fr.acinq.phoenix.android.Screen
+import fr.acinq.phoenix.android.components.Clickable
+import fr.acinq.phoenix.android.components.Dialog
+import fr.acinq.phoenix.android.components.TextWithIcon
+import fr.acinq.phoenix.android.navController
+import fr.acinq.phoenix.android.utils.warningColor
+
+@Composable
+fun BackgroundRestrictionBadge(
+ appViewModel: AppViewModel,
+ isPowerSaverMode: Boolean,
+ isTorEnabled: Boolean,
+) {
+ val isHeadlessModeEnabled = appViewModel.service?.isHeadless == true
+
+ if (isTorEnabled || isPowerSaverMode || !isHeadlessModeEnabled) {
+ var showDialog by remember { mutableStateOf(false) }
+
+ TopBadgeButton(
+ text = null,
+ icon = R.drawable.ic_alert_triangle,
+ iconTint = warningColor,
+ onClick = { showDialog = true },
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+
+ if (showDialog) {
+ Dialog(
+ onDismiss = { showDialog = false },
+ title = stringResource(id = R.string.home_background_restriction_title)
+ ) {
+ Text(
+ text = stringResource(id = R.string.home_background_restriction_body_1),
+ modifier = Modifier.padding(horizontal = 24.dp),
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+
+ if (isTorEnabled) {
+ TextWithIcon(
+ text = stringResource(id = R.string.home_background_restriction_tor),
+ textStyle = MaterialTheme.typography.body2,
+ icon = R.drawable.ic_tor_shield_ok,
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 6.dp),
+ )
+ }
+
+ if (isPowerSaverMode) {
+ TextWithIcon(
+ text = stringResource(id = R.string.home_background_restriction_powersaver),
+ textStyle = MaterialTheme.typography.body2,
+ icon = R.drawable.ic_battery_charging,
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 6.dp)
+ )
+ }
+
+ if (!isHeadlessModeEnabled) {
+ val nc = navController
+ Clickable(onClick = { nc.navigate(Screen.Experimental.route) }) {
+ Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 6.dp)) {
+ TextWithIcon(
+ text = stringResource(id = R.string.home_background_restriction_headless),
+ textStyle = MaterialTheme.typography.body2,
+ icon = R.drawable.ic_phone_tech,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = stringResource(id = R.string.home_background_restriction_headless_desc),
+ style = MaterialTheme.typography.subtitle2,
+ modifier = Modifier.padding(start = 24.dp),
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/services/BootReceiverFoss.kt b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/services/BootReceiverFoss.kt
new file mode 100644
index 000000000..acf2e4986
--- /dev/null
+++ b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/services/BootReceiverFoss.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android.services
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import fr.acinq.phoenix.android.PhoenixApplication
+import fr.acinq.phoenix.legacy.internalData
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import org.slf4j.LoggerFactory
+
+/**
+ * This receiver is started when the device has booted, and schedules background jobs.
+ */
+class BootReceiverFoss : BroadcastReceiver() {
+
+ private val log = LoggerFactory.getLogger(this::class.java)
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
+ ChannelsWatcher.schedule(context)
+ InflightPaymentsWatcher.scheduleOnce(context)
+
+ val internalPrefs = (context as? PhoenixApplication)?.internalDataRepository
+ val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
+ scope.launch {
+ internalPrefs?.getBackgroundServiceMode?.first()?.let { (isEnabled, isWakelock) ->
+ if (isEnabled) {
+ val action = if (isWakelock) HeadlessActions.Start.WithWakeLock else HeadlessActions.Start.NoWakeLock
+ log.info("starting foreground service with action=$action")
+ Intent(context, NodeServiceFoss::class.java).apply {
+ this.action = action.name
+ putExtra(NodeService.EXTRA_ORIGIN, NodeService.ORIGIN_HEADLESS)
+ }
+ context.startForegroundService(intent)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/services/NodeServiceFoss.kt b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/services/NodeServiceFoss.kt
new file mode 100644
index 000000000..635e410be
--- /dev/null
+++ b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/services/NodeServiceFoss.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android.services
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.PowerManager
+import fr.acinq.phoenix.PhoenixBusiness
+import fr.acinq.phoenix.android.security.SeedManager
+import fr.acinq.phoenix.android.utils.SystemNotificationHelper
+
+
+sealed class HeadlessActions {
+ abstract val name: String
+ sealed class Start : HeadlessActions() {
+ data object WithWakeLock : Start() {
+ override val name: String = "START_WITH_WAKELOCK"
+ }
+ data object NoWakeLock : Start() {
+ override val name: String = "START_NO_WAKELOCK"
+ }
+ }
+ data object Stop : HeadlessActions() {
+ override val name: String = "STOP"
+ }
+
+ companion object {
+ fun read(name: String?) : HeadlessActions? {
+ return when (name) {
+ Start.NoWakeLock.name -> Start.NoWakeLock
+ Start.WithWakeLock.name -> Start.WithWakeLock
+ Stop.name -> Stop
+ else -> null
+ }
+ }
+ }
+}
+
+/**
+ * Implementation of [NodeService] without Google-based FCM support. Instead, to receive payments in the
+ * background, the user must first foreground this service manually and let the app run headless.
+ *
+ * Uses a [ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING] foreground type.
+ *
+ * See https://github.com/robertohuertasm/endless-service
+ */
+class NodeServiceFoss : NodeService() {
+
+ override suspend fun monitorFcmToken(business: PhoenixBusiness) {} // NOOP
+ override fun refreshFcmToken() {} // NOOP
+ override fun deleteFcmToken() {} // NOOP
+ override fun isFcmAvailable(context: Context): Boolean = false
+
+ var wakeLock: PowerManager.WakeLock? = null
+ private set
+
+ @SuppressLint("InlinedApi")
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+ val origin = intent?.getStringExtra(EXTRA_ORIGIN)
+ when (val action = HeadlessActions.read(intent?.action)) {
+ is HeadlessActions.Start -> {
+ log.info("--- start headless mode ---")
+ val (notif, serviceType) = when (state.value) {
+ is NodeServiceState.Off -> {
+ try {
+ val encryptedSeed = SeedManager.loadSeedFromDisk(applicationContext)
+ val seed = encryptedSeed!!.decrypt()
+ log.debug("successfully decrypted seed in the background, starting wallet...")
+ startBusiness(seed, requestCheckLegacyChannels = false)
+ SystemNotificationHelper.getHeadlessNotification(applicationContext) to ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
+ } catch (e: Exception) {
+ log.error("failed to decrypt seed: {}", e.localizedMessage)
+ SystemNotificationHelper.getHeadlessFailureNotification(applicationContext) to ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
+ }
+ }
+ else -> {
+ SystemNotificationHelper.getHeadlessNotification(applicationContext) to ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
+ }
+ }
+ isHeadless = true
+ startForeground(notif, serviceType)
+ val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
+ if (action is HeadlessActions.Start.WithWakeLock) {
+ log.info("--- acquire wakelock ---")
+ wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PhoenixService::lock")
+ .apply { acquire() }
+ }
+ return START_STICKY
+ }
+ is HeadlessActions.Stop -> {
+ log.info("--- stop headless mode ---")
+ isHeadless = false
+ if (wakeLock?.isHeld == true) {
+ log.info("--- release wakelock ---")
+ wakeLock?.release()
+ }
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ return START_NOT_STICKY
+ }
+ else -> {
+ log.info("--- unhandled start-command [ intent=${intent?.action} origin=$origin in state=${state.value} ] ---")
+ return START_NOT_STICKY
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/settings/ExperimentalViewFoss.kt b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/settings/ExperimentalViewFoss.kt
new file mode 100644
index 000000000..ee4554cfd
--- /dev/null
+++ b/phoenix-android/src/foss/kotlin/fr/acinq/phoenix/android/settings/ExperimentalViewFoss.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2024 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android.settings
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.rememberModalBottomSheetState
+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 fr.acinq.phoenix.android.AppViewModel
+import fr.acinq.phoenix.android.MainActivityFoss
+import fr.acinq.phoenix.android.R
+import fr.acinq.phoenix.android.components.Card
+import fr.acinq.phoenix.android.components.CardHeader
+import fr.acinq.phoenix.android.components.FilledButton
+import fr.acinq.phoenix.android.components.SwitchView
+import fr.acinq.phoenix.android.components.settings.SettingSwitch
+import fr.acinq.phoenix.android.internalData
+import fr.acinq.phoenix.android.services.HeadlessActions
+import fr.acinq.phoenix.android.services.NodeServiceFoss
+import fr.acinq.phoenix.android.utils.findActivity
+import kotlinx.coroutines.launch
+
+@Composable
+fun ManageHeadlessView(appViewModel: AppViewModel) {
+ CardHeader(text = stringResource(id = R.string.background_header))
+ Card(modifier = Modifier.fillMaxWidth()) {
+ val service = appViewModel.service as NodeServiceFoss?
+ val activity = LocalContext.current.findActivity() as MainActivityFoss
+ val scope = rememberCoroutineScope()
+ val internalPrefs = internalData
+
+ var showConfirmDialog by remember { mutableStateOf(false) }
+
+ SettingSwitch(
+ title = {
+ Text(
+ text = stringResource(id = R.string.background_mode_title),
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier.weight(1f)
+ )
+ },
+ subtitle = when (service?.isHeadless) {
+ null -> null
+ true -> if (service.wakeLock?.isHeld == true) {
+ { Text(stringResource(id = R.string.background_mode_on_wakelock), style = MaterialTheme.typography.subtitle2) }
+ } else {
+ { Text(stringResource(id = R.string.background_mode_on_no_wakelock), style = MaterialTheme.typography.subtitle2) }
+ }
+ false -> {
+ { Text(text = stringResource(id = R.string.background_mode_off), style = MaterialTheme.typography.subtitle2) }
+ }
+ },
+ icon = R.drawable.ic_phone_tech,
+ enabled = service?.isHeadless != null,
+ isChecked = service?.isHeadless ?: false,
+ onCheckChangeAttempt = { runHeadless ->
+ scope.launch {
+ if (runHeadless) {
+ showConfirmDialog = true
+ } else {
+ scope.launch {
+ activity.headlessServiceAction(HeadlessActions.Stop)
+ internalPrefs.saveBackgroundServiceModeDisabled()
+ }
+ }
+ }
+ }
+ )
+
+ if (showConfirmDialog) {
+ ConfirmHeadlessDialog(
+ onDismiss = { showConfirmDialog = false },
+ onConfirm = { withWakeLock ->
+ scope.launch {
+ activity.headlessServiceAction(if (withWakeLock) HeadlessActions.Start.WithWakeLock else HeadlessActions.Start.NoWakeLock)
+ internalPrefs.saveBackgroundServiceModeEnabled(withWakeLock)
+ showConfirmDialog = false
+ }
+ }
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun ConfirmHeadlessDialog(
+ onDismiss: () -> Unit,
+ onConfirm: (Boolean) -> Unit,
+) {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+
+ ModalBottomSheet(
+ sheetState = sheetState,
+ onDismissRequest = onDismiss,
+ containerColor = MaterialTheme.colors.surface,
+ contentColor = MaterialTheme.colors.onSurface,
+ scrimColor = MaterialTheme.colors.onBackground.copy(alpha = 0.2f),
+ ) {
+ var withWakeLock by remember { mutableStateOf(true) }
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .sizeIn(minHeight = 400.dp, maxHeight = 700.dp)
+ .padding(horizontal = 24.dp),
+ ) {
+ Text(text = stringResource(id = R.string.background_mode_dialog_title), style = MaterialTheme.typography.h4)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = stringResource(id = R.string.background_mode_dialog_desc))
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = stringResource(id = R.string.background_mode_dialog_perfs_desc))
+ Spacer(modifier = Modifier.height(12.dp))
+ SwitchView(
+ text = stringResource(id = R.string.background_mode_dialog_wakelock_title),
+ textStyle = MaterialTheme.typography.body2,
+ description = stringResource(id = R.string.background_mode_dialog_wakelock_desc),
+ checked = withWakeLock,
+ onCheckedChange = { withWakeLock = it }
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(text = stringResource(id = R.string.background_mode_dialog_alternative_title), style = MaterialTheme.typography.h4)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = stringResource(id = R.string.background_mode_dialog_alternative_desc))
+ Spacer(modifier = Modifier.height(32.dp))
+ FilledButton(
+ text = stringResource(id = R.string.btn_start),
+ icon = R.drawable.ic_start,
+ onClick = { onConfirm(withWakeLock) },
+ modifier = Modifier.align(Alignment.End)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ FilledButton(
+ text = stringResource(id = R.string.btn_cancel),
+ icon = R.drawable.ic_check,
+ onClick = onDismiss,
+ modifier = Modifier.align(Alignment.End),
+ backgroundColor = Color.Transparent,
+ textStyle = MaterialTheme.typography.button
+ )
+ Spacer(modifier = Modifier.height(40.dp))
+ }
+ }
+}
\ No newline at end of file
diff --git a/phoenix-android/src/foss/res/drawable/ic_phone_tech.xml b/phoenix-android/src/foss/res/drawable/ic_phone_tech.xml
new file mode 100644
index 000000000..11e6587a4
--- /dev/null
+++ b/phoenix-android/src/foss/res/drawable/ic_phone_tech.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phoenix-android/src/foss/res/drawable/ic_start.xml b/phoenix-android/src/foss/res/drawable/ic_start.xml
new file mode 100644
index 000000000..578b68719
--- /dev/null
+++ b/phoenix-android/src/foss/res/drawable/ic_start.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
diff --git a/phoenix-android/src/foss/res/values/strings.xml b/phoenix-android/src/foss/res/values/strings.xml
new file mode 100644
index 000000000..929eb1312
--- /dev/null
+++ b/phoenix-android/src/foss/res/values/strings.xml
@@ -0,0 +1,36 @@
+
+
+
+ Background service mode is disabled
+ Tap for details.
+
+ Background processing
+ Background service mode
+ Not running. Enable to improve payments reliability
+ Service is running (wakelock enabled)
+ Service is running
+
+ Always-on service
+ When enabled, Phoenix will keep running as a background service even if the app is closed, allowing you to receive payments more reliably.
+ Performance cost
+ Phoenix battery usage will increase significantly.
+ Keep wake-lock
+ Makes the service more reliable, but uses much more battery.
+ Alternatives
+ If you prefer optimised, just-in-time notifications for background processing, install the Google Play version of Phoenix instead.
+
+
\ No newline at end of file
diff --git a/phoenix-android/src/google/AndroidManifest.xml b/phoenix-android/src/google/AndroidManifest.xml
new file mode 100644
index 000000000..a19327c42
--- /dev/null
+++ b/phoenix-android/src/google/AndroidManifest.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/FCMHelper.kt b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/MainActivityGoogle.kt
similarity index 59%
rename from phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/FCMHelper.kt
rename to phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/MainActivityGoogle.kt
index 05944a3b0..df6ef9eeb 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/FCMHelper.kt
+++ b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/MainActivityGoogle.kt
@@ -14,14 +14,17 @@
* limitations under the License.
*/
-package fr.acinq.phoenix.android.utils
+package fr.acinq.phoenix.android
import android.content.Context
-import com.google.android.gms.common.ConnectionResult
-import com.google.android.gms.common.GoogleApiAvailability
+import android.content.Intent
+import fr.acinq.phoenix.android.services.NodeServiceGoogle
-object FCMHelper {
- fun isFCMAvailable(context: Context): Boolean {
- return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+class MainActivityGoogle : MainActivity() {
+
+ override fun bindService() {
+ Intent(this, NodeServiceGoogle::class.java).let { intent ->
+ applicationContext.bindService(intent, appViewModel.serviceConnection, Context.BIND_AUTO_CREATE or Context.BIND_ADJUST_WITH_ACTIVITY)
+ }
}
}
\ No newline at end of file
diff --git a/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/PhoenixApplicationGoogle.kt b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/PhoenixApplicationGoogle.kt
new file mode 100644
index 000000000..757b8fc3a
--- /dev/null
+++ b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/PhoenixApplicationGoogle.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android
+
+import fr.acinq.phoenix.android.services.NodeService
+import fr.acinq.phoenix.android.services.NodeServiceGoogle
+
+
+class PhoenixApplicationGoogle : PhoenixApplication() {
+
+ override val mainActivityClass: Class get() = MainActivityGoogle::class.java
+ override val nodeServiceClass: Class get() = NodeServiceGoogle::class.java
+}
diff --git a/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/home/BackgroundRestrictionBadgeGoogle.kt b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/home/BackgroundRestrictionBadgeGoogle.kt
new file mode 100644
index 000000000..038431a5e
--- /dev/null
+++ b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/home/BackgroundRestrictionBadgeGoogle.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2024 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android.home
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.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.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import fr.acinq.phoenix.android.AppViewModel
+import fr.acinq.phoenix.android.R
+import fr.acinq.phoenix.android.components.Dialog
+import fr.acinq.phoenix.android.components.TextWithIcon
+import fr.acinq.phoenix.android.utils.warningColor
+
+@Composable
+fun BackgroundRestrictionBadge(
+ appViewModel: AppViewModel,
+ isPowerSaverMode: Boolean,
+ isTorEnabled: Boolean,
+) {
+ val context = LocalContext.current
+ val isFcmAvailable = appViewModel.service?.isFcmAvailable(context) == true
+
+ if (isTorEnabled || isPowerSaverMode || !isFcmAvailable) {
+ var showDialog by remember { mutableStateOf(false) }
+
+ TopBadgeButton(
+ text = null,
+ icon = R.drawable.ic_alert_triangle,
+ iconTint = warningColor,
+ onClick = { showDialog = true },
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+
+ if (showDialog) {
+ Dialog(
+ onDismiss = { showDialog = false },
+ title = stringResource(id = R.string.home_background_restriction_title)
+ ) {
+ Text(
+ text = stringResource(id = R.string.home_background_restriction_body_1),
+ modifier = Modifier.padding(horizontal = 24.dp),
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+
+ if (isTorEnabled) {
+ TextWithIcon(
+ text = stringResource(id = R.string.home_background_restriction_tor),
+ textStyle = MaterialTheme.typography.body2,
+ icon = R.drawable.ic_tor_shield_ok,
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 6.dp)
+ )
+ }
+
+ if (isPowerSaverMode) {
+ TextWithIcon(
+ text = stringResource(id = R.string.home_background_restriction_powersaver),
+ textStyle = MaterialTheme.typography.body2,
+ icon = R.drawable.ic_battery_charging,
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 6.dp)
+ )
+ }
+
+ if (!isFcmAvailable) {
+ Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 6.dp)) {
+ TextWithIcon(
+ text = stringResource(id = R.string.home_background_restriction_fcm),
+ textStyle = MaterialTheme.typography.body2,
+ icon = R.drawable.ic_battery_charging,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = stringResource(id = R.string.home_background_restriction_fcm_details),
+ style = MaterialTheme.typography.subtitle2,
+ modifier = Modifier.padding(start = 24.dp),
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/FCMService.kt b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/services/FCMService.kt
similarity index 93%
rename from phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/FCMService.kt
rename to phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/services/FCMService.kt
index 98c24c0bc..cd1662a0c 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/FCMService.kt
+++ b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/services/FCMService.kt
@@ -35,7 +35,7 @@ class FCMService : FirebaseMessagingService() {
when {
encryptedSeed !is EncryptedSeed.V2.NoAuth -> {
- log.warn("ignored fcm message with unhandled seed=${encryptedSeed?.name()}")
+ log.warn("ignored fcm message with unhandled seed}")
}
remoteMessage.priority != RemoteMessage.PRIORITY_HIGH -> {
// cannot start foreground service from low/normal priority message
@@ -54,8 +54,11 @@ class FCMService : FirebaseMessagingService() {
}
private fun startPhoenixForegroundService(reason: String?) {
- ContextCompat.startForegroundService(applicationContext, Intent(applicationContext, NodeService::class.java)
- .apply { reason?.let { putExtra(NodeService.EXTRA_REASON, it) } })
+ ContextCompat.startForegroundService(applicationContext, Intent(applicationContext, NodeServiceGoogle::class.java)
+ .apply {
+ reason?.let { putExtra(NodeService.EXTRA_REASON, it) }
+ putExtra(NodeService.EXTRA_ORIGIN, NodeService.ORIGIN_FCM)
+ })
}
override fun onNewToken(token: String) {
diff --git a/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/services/NodeServiceGoogle.kt b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/services/NodeServiceGoogle.kt
new file mode 100644
index 000000000..fbdc714e2
--- /dev/null
+++ b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/services/NodeServiceGoogle.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2024 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android.services
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.google.android.gms.tasks.OnCompleteListener
+import com.google.firebase.messaging.FirebaseMessaging
+import fr.acinq.lightning.utils.Connection
+import fr.acinq.phoenix.PhoenixBusiness
+import fr.acinq.phoenix.android.security.SeedManager
+import fr.acinq.phoenix.android.utils.SystemNotificationHelper
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+
+class NodeServiceGoogle : NodeService() {
+
+ override fun onBind(intent: Intent?): IBinder {
+ isHeadless = false
+ notificationManager.cancel(SystemNotificationHelper.HEADLESS_NOTIF_ID)
+ return super.onBind(intent)
+ }
+
+ override fun onUnbind(intent: Intent?): Boolean {
+ isHeadless = true
+ return super.onUnbind(intent)
+ }
+
+ override suspend fun monitorFcmToken(business: PhoenixBusiness) {
+ val token = internalData.getFcmToken.filterNotNull().first()
+ business.connectionsManager.connections.first { it.peer == Connection.ESTABLISHED }
+ business.registerFcmToken(token)
+ }
+
+ override fun refreshFcmToken() {
+ FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
+ if (!task.isSuccessful) {
+ log.warn("fetching FCM registration token failed: ${task.exception?.localizedMessage}")
+ return@OnCompleteListener
+ }
+ task.result?.let { serviceScope.launch { internalData.saveFcmToken(it) } }
+ })
+ }
+
+ override fun deleteFcmToken() {
+ FirebaseMessaging.getInstance().deleteToken().addOnCompleteListener { task ->
+ if (task.isSuccessful) refreshFcmToken()
+ }
+ }
+
+ override fun isFcmAvailable(context: Context): Boolean {
+ return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+ }
+
+ // See [scheduleShutdown].
+ private val shutdownHandler = Handler(Looper.getMainLooper())
+
+ /**
+ * Schedules business shutdown and removal of service from the foreground if needed.
+ * After receiving a FCM wake-up message, the service is put in the foreground and then keeps running
+ * for a while (typically 2 minutes). Then it shuts down automatically.
+ *
+ * @param delayMillis default is 2 minutes. Should always be less than 3 minutes, to avoid [onTimeout] since
+ * foreground mode is using the `SHORT_SERVICE` foreground type.
+ */
+ private fun scheduleShutdown(delayMillis: Long = 2 * 60 * 1000L) {
+ shutdownHandler.postDelayed(Runnable {
+ if (isHeadless) {
+ log.debug("reached scheduled shutdown...")
+ if (receivedInBackground.isEmpty()) {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ } else {
+ stopForeground(STOP_FOREGROUND_DETACH)
+ }
+ notificationManager.cancel(SystemNotificationHelper.HEADLESS_NOTIF_ID)
+ shutdown()
+ }
+ }, delayMillis)
+ }
+
+ @SuppressLint("InlinedApi")
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+ val origin = intent?.getStringExtra(EXTRA_ORIGIN)
+ val reason = intent?.getStringExtra(EXTRA_REASON)
+ log.info("--- start service from intent [ intent=$intent, flag=$flags, startId=$startId ] reason=$reason origin=$origin in state=${state.value} ---")
+
+ val encryptedSeed = SeedManager.loadSeedFromDisk(applicationContext)
+ val (notif, serviceType) = when (state.value) {
+ is NodeServiceState.Off -> {
+ try {
+ val seed = encryptedSeed!!.decrypt()
+ log.debug("successfully decrypted seed in the background, starting wallet...")
+ startBusiness(seed, requestCheckLegacyChannels = false)
+ SystemNotificationHelper.getHeadlessNotification(applicationContext) to ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
+ } catch (e: Exception) {
+ log.error("failed to decrypt seed: {}", e.localizedMessage)
+ SystemNotificationHelper.getHeadlessFailureNotification(applicationContext) to ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
+ }
+ }
+ else -> {
+ SystemNotificationHelper.getHeadlessNotification(applicationContext) to ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
+ // TODO force reconnect if disconnected!
+ }
+ }
+
+ startForeground(notif, serviceType)
+ shutdownHandler.removeCallbacksAndMessages(null)
+ scheduleShutdown()
+
+ if (origin == ORIGIN_FCM) {
+ isHeadless = true
+ } else {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
+ return START_NOT_STICKY
+ }
+}
\ No newline at end of file
diff --git a/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/settings/ExperimentalViewGoogle.kt b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/settings/ExperimentalViewGoogle.kt
new file mode 100644
index 000000000..723048e4f
--- /dev/null
+++ b/phoenix-android/src/google/kotlin/fr/acinq/phoenix/android/settings/ExperimentalViewGoogle.kt
@@ -0,0 +1,7 @@
+package fr.acinq.phoenix.android.settings
+
+import androidx.compose.runtime.Composable
+import fr.acinq.phoenix.android.AppViewModel
+
+@Composable
+fun ManageHeadlessView(appViewModel: AppViewModel) {}
\ No newline at end of file
diff --git a/phoenix-android/src/main/AndroidManifest.xml b/phoenix-android/src/main/AndroidManifest.xml
index e6518cdaa..3eef01ece 100644
--- a/phoenix-android/src/main/AndroidManifest.xml
+++ b/phoenix-android/src/main/AndroidManifest.xml
@@ -11,7 +11,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt
index c5af26631..70fa3c1be 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt
@@ -58,7 +58,6 @@ import androidx.navigation.navOptions
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
-import com.google.firebase.messaging.FirebaseMessaging
import fr.acinq.lightning.db.IncomingPayment
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.PhoenixBusiness
@@ -136,7 +135,7 @@ fun LoadingAppView() {
@Composable
fun AppView(
business: PhoenixBusiness,
- appVM: AppViewModel,
+ appViewModel: AppViewModel,
navController: NavHostController,
) {
val log = logger("Navigation")
@@ -172,7 +171,7 @@ fun AppView(
)
MonitorNotices(vm = noticesViewModel)
- val walletState by appVM.serviceState.observeAsState(null)
+ val walletState by appViewModel.serviceState.observeAsState(null)
Box(modifier = Modifier.fillMaxSize()) {
Column(
@@ -202,7 +201,7 @@ fun AppView(
null
}
StartupView(
- appVM = appVM,
+ appVM = appViewModel,
onShowIntro = { navController.navigate(Screen.Intro.route) },
onKeyAbsent = { navController.navigate(Screen.InitWallet.route) },
onBusinessStarted = {
@@ -237,6 +236,7 @@ fun AppView(
composable(Screen.Home.route) {
RequireStarted(walletState) {
HomeView(
+ appViewModel = appViewModel,
paymentsViewModel = paymentsViewModel,
noticesViewModel = noticesViewModel,
onPaymentClick = { navigateToPaymentDetails(navController, id = it, isFromEvent = false) },
@@ -415,7 +415,7 @@ fun AppView(
)
}
composable(Screen.AppLock.route) {
- AppAccessSettings(onBackClick = { navController.popBackStack() }, appViewModel = appVM)
+ AppAccessSettings(onBackClick = { navController.popBackStack() }, appViewModel = appViewModel)
}
composable(Screen.Logs.route) {
LogsView()
@@ -477,7 +477,7 @@ fun AppView(
)
}
composable(Screen.ResetWallet.route) {
- appVM.service?.let { nodeService ->
+ appViewModel.service?.let { nodeService ->
val application = application
ResetWallet(
onShutdownBusiness = application::shutdownBusiness,
@@ -485,9 +485,7 @@ fun AppView(
onPrefsClear = application::clearPreferences,
onBusinessReset = {
application.resetBusiness()
- FirebaseMessaging.getInstance().deleteToken().addOnCompleteListener { task ->
- if (task.isSuccessful) nodeService.refreshFcmToken()
- }
+ nodeService.deleteFcmToken()
},
onBackClick = { navController.popBackStack() }
)
@@ -497,7 +495,7 @@ fun AppView(
SettingsContactsView(onBackClick = { navController.popBackStack() })
}
composable(Screen.Experimental.route) {
- ExperimentalView(onBackClick = { navController.popBackStack() })
+ ExperimentalView(onBackClick = { navController.popBackStack() }, appViewModel = appViewModel)
}
}
@@ -507,7 +505,7 @@ fun AppView(
}
}
- val isScreenLocked by appVM.isScreenLocked
+ val isScreenLocked by appViewModel.isScreenLocked
val isBiometricLockEnabledState = userPrefs.getIsBiometricLockEnabled.collectAsState(initial = null)
val isBiometricLockEnabled = isBiometricLockEnabledState.value
val isCustomPinLockEnabledState = userPrefs.getIsCustomPinLockEnabled.collectAsState(initial = null)
@@ -519,11 +517,11 @@ fun AppView(
context.safeFindActivity()?.moveTaskToBack(false)
}
LockPrompt(
- promptScreenLockImmediately = appVM.promptScreenLockImmediately.value,
- onLock = { appVM.lockScreen() },
+ promptScreenLockImmediately = appViewModel.promptScreenLockImmediately.value,
+ onLock = { appViewModel.lockScreen() },
onUnlock = {
- appVM.unlockScreen()
- appVM.promptScreenLockImmediately.value = false
+ appViewModel.unlockScreen()
+ appViewModel.promptScreenLockImmediately.value = false
},
)
}
@@ -539,7 +537,7 @@ fun AppView(
if (isDataMigrationExpected == false) {
if (payment is IncomingPayment && payment.origin is IncomingPayment.Origin.Offer) {
SystemNotificationHelper.notifyPaymentsReceived(
- context, userPrefs, paymentHash = payment.paymentHash, amount = payment.amount, rates = exchangeRates, isHeadless = false
+ context, userPrefs, paymentHash = payment.paymentHash, amount = payment.amount, rates = exchangeRates, isFromBackground = false
)
} else {
navigateToPaymentDetails(navController, id = payment.walletPaymentId(), isFromEvent = true)
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/MainActivity.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/MainActivity.kt
index dec556168..49a880b5a 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/MainActivity.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/MainActivity.kt
@@ -31,7 +31,6 @@ import androidx.navigation.*
import androidx.navigation.compose.rememberNavController
import fr.acinq.lightning.io.PhoenixAndroidLegacyInfoEvent
import fr.acinq.lightning.utils.Connection
-import fr.acinq.phoenix.android.services.NodeService
import fr.acinq.phoenix.android.utils.LegacyMigrationHelper
import fr.acinq.phoenix.android.utils.PhoenixAndroidTheme
import fr.acinq.phoenix.legacy.utils.*
@@ -47,10 +46,10 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
-class MainActivity : AppCompatActivity() {
+abstract class MainActivity : AppCompatActivity() {
val log: Logger = LoggerFactory.getLogger(MainActivity::class.java)
- private val appViewModel: AppViewModel by viewModels { AppViewModel.Factory }
+ internal val appViewModel: AppViewModel by viewModels { AppViewModel.Factory }
private var navController: NavHostController? = null
@@ -151,9 +150,7 @@ class MainActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
- Intent(this, NodeService::class.java).let { intent ->
- applicationContext.bindService(intent, appViewModel.serviceConnection, Context.BIND_AUTO_CREATE or Context.BIND_ADJUST_WITH_ACTIVITY)
- }
+ bindService()
}
override fun onResume() {
@@ -161,6 +158,8 @@ class MainActivity : AppCompatActivity() {
tryReconnect()
}
+ abstract fun bindService()
+
private fun tryReconnect() {
lifecycleScope.launch {
val isDataMigrationExpected = LegacyPrefsDatastore.getDataMigrationExpected(applicationContext).filterNotNull().first()
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/PhoenixApplication.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/PhoenixApplication.kt
index 3bbd00d81..22857a479 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/PhoenixApplication.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/PhoenixApplication.kt
@@ -18,6 +18,7 @@ package fr.acinq.phoenix.android
import android.content.Intent
import fr.acinq.phoenix.PhoenixBusiness
+import fr.acinq.phoenix.android.services.NodeService
import fr.acinq.phoenix.android.utils.Logging
import fr.acinq.phoenix.android.utils.SystemNotificationHelper
import fr.acinq.phoenix.android.utils.datastore.InternalDataRepository
@@ -31,7 +32,7 @@ import fr.acinq.phoenix.utils.PlatformContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-class PhoenixApplication : AppContext() {
+abstract class PhoenixApplication : AppContext() {
private val _business = MutableStateFlow(null)
val business = _business.asStateFlow()
@@ -39,6 +40,9 @@ class PhoenixApplication : AppContext() {
lateinit var internalDataRepository: InternalDataRepository
lateinit var userPrefs: UserPrefsRepository
+ abstract val mainActivityClass: Class
+ abstract val nodeServiceClass: Class
+
override fun onCreate() {
super.onCreate()
_business.value = PhoenixBusiness(PlatformContext(applicationContext))
@@ -49,7 +53,7 @@ class PhoenixApplication : AppContext() {
}
override fun onLegacyFinish() {
- applicationContext.startActivity(Intent(applicationContext, MainActivity::class.java).apply {
+ applicationContext.startActivity(Intent(applicationContext, mainActivityClass).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
})
}
@@ -59,6 +63,7 @@ class PhoenixApplication : AppContext() {
business.value?.appConnectionsDaemon?.incrementDisconnectCount(AppConnectionsDaemon.ControlTarget.All)
business.value?.stop()
}
+
fun resetBusiness() {
_business.value = PhoenixBusiness(PlatformContext(this))
log.info("business=$business")
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/settings/SettingSwitch.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/settings/SettingSwitch.kt
index e346e8d5c..a576e133f 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/settings/SettingSwitch.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/settings/SettingSwitch.kt
@@ -19,6 +19,7 @@ package fr.acinq.phoenix.android.components.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -46,6 +47,32 @@ fun SettingSwitch(
enabled: Boolean,
isChecked: Boolean,
onCheckChangeAttempt: ((Boolean) -> Unit)
+) {
+ SettingSwitch(
+ modifier = modifier,
+ title = {
+ Text(text = title, style = if (enabled) MaterialTheme.typography.body2 else MaterialTheme.typography.caption, modifier = Modifier.weight(1f))
+ },
+ subtitle = description?.let {
+ { Text(text = description, style = MaterialTheme.typography.subtitle2, modifier = Modifier.weight(1f)) }
+ },
+ icon = icon,
+ enabled = enabled,
+ isChecked = isChecked,
+ onCheckChangeAttempt = onCheckChangeAttempt
+ )
+}
+
+
+@Composable
+fun SettingSwitch(
+ modifier: Modifier = Modifier,
+ title: @Composable RowScope.() -> Unit,
+ subtitle: (@Composable RowScope.() -> Unit)? = null,
+ icon: Int? = null,
+ enabled: Boolean,
+ isChecked: Boolean,
+ onCheckChangeAttempt: ((Boolean) -> Unit)
) {
Column(
modifier
@@ -58,17 +85,17 @@ fun SettingSwitch(
PhoenixIcon(it, Modifier.size(ButtonDefaults.IconSize))
Spacer(Modifier.width(12.dp))
}
- Text(text = title, style = if (enabled) MaterialTheme.typography.body2 else MaterialTheme.typography.caption, modifier = Modifier.weight(1f))
+ title()
Spacer(Modifier.width(16.dp))
Switch(checked = isChecked, onCheckedChange = null, enabled = enabled, modifier = Modifier.enableOrFade(enabled))
}
- if (description != null) {
+ if (subtitle != null) {
Spacer(modifier = Modifier.height(2.dp))
Row(Modifier.fillMaxWidth()) {
icon?.let {
Spacer(modifier = Modifier.width(30.dp))
}
- Text(text = description, style = MaterialTheme.typography.subtitle2, modifier = Modifier.weight(1f))
+ subtitle()
Spacer(Modifier.width(48.dp))
}
}
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt
index a178d829b..8f8fc145a 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt
@@ -16,9 +16,22 @@
package fr.acinq.phoenix.android.home
-import androidx.compose.animation.core.*
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
@@ -40,12 +53,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.acinq.lightning.utils.Connection
+import fr.acinq.phoenix.android.AppViewModel
import fr.acinq.phoenix.android.R
import fr.acinq.phoenix.android.components.BorderButton
import fr.acinq.phoenix.android.components.Button
import fr.acinq.phoenix.android.components.Dialog
import fr.acinq.phoenix.android.components.FilledButton
-import fr.acinq.phoenix.android.components.TextWithIcon
import fr.acinq.phoenix.android.components.VSeparator
import fr.acinq.phoenix.android.components.openLink
import fr.acinq.phoenix.android.utils.isBadCertificate
@@ -57,12 +70,12 @@ import fr.acinq.phoenix.managers.Connections
@Composable
fun TopBar(
modifier: Modifier = Modifier,
+ appViewModel: AppViewModel,
onConnectionsStateButtonClick: () -> Unit,
connections: Connections,
electrumBlockheight: Int,
onTorClick: () -> Unit,
isTorEnabled: Boolean?,
- isFCMUnavailable: Boolean,
isPowerSaverMode: Boolean,
inFlightPaymentsCount: Int,
showRequestLiquidity: Boolean,
@@ -90,9 +103,9 @@ fun TopBar(
}
BackgroundRestrictionBadge(
- isFCMUnavailable = isFCMUnavailable,
+ appViewModel = appViewModel,
+ isPowerSaverMode = isPowerSaverMode,
isTorEnabled = isTorEnabled == true,
- isPowerSaverMode = isPowerSaverMode
)
Spacer(modifier = Modifier.weight(1f))
@@ -172,7 +185,7 @@ private fun ConnectionBadge(
}
@Composable
-private fun TopBadgeButton(
+fun TopBadgeButton(
text: String?,
icon: Int,
onClick: () -> Unit,
@@ -193,58 +206,6 @@ private fun TopBadgeButton(
)
}
-@Composable
-private fun BackgroundRestrictionBadge(
- isTorEnabled: Boolean,
- isPowerSaverMode: Boolean,
- isFCMUnavailable: Boolean,
-) {
- if (isTorEnabled || isPowerSaverMode || isFCMUnavailable) {
- var showDialog by remember { mutableStateOf(false) }
-
- TopBadgeButton(
- text = null,
- icon = R.drawable.ic_alert_triangle,
- iconTint = warningColor,
- onClick = { showDialog = true },
- )
- Spacer(modifier = Modifier.width(4.dp))
-
- if (showDialog) {
- Dialog(
- onDismiss = { showDialog = false },
- title = stringResource(id = R.string.home_background_restriction_title)
- ) {
- Column(
- modifier = Modifier.padding(horizontal = 24.dp)
- ) {
- Text(text = stringResource(id = R.string.home_background_restriction_body_1))
- Spacer(modifier = Modifier.height(16.dp))
- Text(text = stringResource(id = R.string.home_background_restriction_body_2))
- Spacer(modifier = Modifier.height(8.dp))
-
- Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- if (isTorEnabled) {
- TextWithIcon(text = stringResource(id = R.string.home_background_restriction_tor), icon = R.drawable.ic_tor_shield_ok)
- }
- if (isPowerSaverMode) {
- TextWithIcon(text = stringResource(id = R.string.home_background_restriction_powersaver), icon = R.drawable.ic_battery_charging)
- }
- if (isFCMUnavailable) {
- TextWithIcon(text = stringResource(id = R.string.home_background_restriction_fcm), icon = R.drawable.ic_cloud_off)
- Text(
- text = stringResource(id = R.string.home_background_restriction_fcm_details),
- style = MaterialTheme.typography.caption.copy(fontSize = 14.sp),
- modifier = Modifier.padding(start = 26.dp)
- )
- }
- }
- }
- }
- }
- }
-}
-
@Composable
private fun InflightPaymentsBadge(
count: Int,
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeView.kt
index 5303cd004..8683fd7ac 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeView.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeView.kt
@@ -44,6 +44,7 @@ import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.MotionLayout
import androidx.constraintlayout.compose.MotionScene
import androidx.constraintlayout.compose.layoutId
+import fr.acinq.phoenix.android.AppViewModel
import fr.acinq.phoenix.android.CF
import fr.acinq.phoenix.android.NoticesViewModel
import fr.acinq.phoenix.android.PaymentsViewModel
@@ -53,7 +54,6 @@ import fr.acinq.phoenix.android.business
import fr.acinq.phoenix.android.components.Dialog
import fr.acinq.phoenix.android.components.PrimarySeparator
import fr.acinq.phoenix.android.components.mvi.MVIView
-import fr.acinq.phoenix.android.utils.FCMHelper
import fr.acinq.phoenix.android.utils.annotatedStringResource
import fr.acinq.phoenix.android.utils.datastore.HomeAmountDisplayMode
import fr.acinq.phoenix.android.utils.findActivity
@@ -67,6 +67,7 @@ import kotlinx.coroutines.launch
@Composable
fun HomeView(
+ appViewModel: AppViewModel,
paymentsViewModel: PaymentsViewModel,
noticesViewModel: NoticesViewModel,
onPaymentClick: (WalletPaymentId) -> Unit,
@@ -85,8 +86,6 @@ fun HomeView(
val internalData = application.internalDataRepository
val userPrefs = application.userPrefs
val isPowerSaverModeOn = noticesViewModel.isPowerSaverModeOn
- val fcmToken by internalData.getFcmToken.collectAsState(initial = "")
- val isFCMAvailable = remember { FCMHelper.isFCMAvailable(context) }
val torEnabledState = userPrefs.getIsTorEnabled.collectAsState(initial = null)
val balanceDisplayMode by userPrefs.getHomeAmountDisplayMode.collectAsState(initial = HomeAmountDisplayMode.REDACTED)
@@ -221,13 +220,13 @@ fun HomeView(
) {}
TopBar(
modifier = Modifier.layoutId("topBar"),
+ appViewModel = appViewModel,
onConnectionsStateButtonClick = { showConnectionsDialog = true },
connections = connections,
electrumBlockheight = electrumMessages?.blockHeight ?: 0,
inFlightPaymentsCount = inFlightPaymentsCount,
isTorEnabled = torEnabledState.value,
onTorClick = onTorClick,
- isFCMUnavailable = fcmToken == null || !isFCMAvailable,
isPowerSaverMode = isPowerSaverModeOn,
showRequestLiquidity = channels.canRequestLiquidity(),
onRequestLiquidityClick = onRequestLiquidityClick,
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/EncryptedSeed.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/EncryptedSeed.kt
index 7bb4d4f49..13c8f4608 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/EncryptedSeed.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/EncryptedSeed.kt
@@ -73,7 +73,7 @@ sealed class EncryptedSeed {
private const val NO_AUTH_KEY_VERSION = 1
private const val REMOVED_DO_NOT_USE = 2
- fun deserialize(stream: ByteArrayInputStream): V2 {
+ fun deserialize(stream: ByteArrayInputStream): V2.NoAuth {
val keyVersion = stream.read()
val iv = ByteArray(IV_LENGTH)
stream.read(iv, 0, IV_LENGTH)
@@ -93,7 +93,7 @@ sealed class EncryptedSeed {
const val SEED_FILE_VERSION_2: Byte = 2
/** Reads an array of byte and de-serializes it as an [EncryptedSeed] object. */
- fun deserialize(serialized: ByteArray): EncryptedSeed {
+ fun deserialize(serialized: ByteArray): EncryptedSeed.V2.NoAuth {
val stream = ByteArrayInputStream(serialized)
val version = stream.read()
return when (version) {
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/SeedManager.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/SeedManager.kt
index c30bd38ca..db0802044 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/SeedManager.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/SeedManager.kt
@@ -42,7 +42,7 @@ object SeedManager {
}
/** Extract the encrypted seed from app private dir. */
- fun loadSeedFromDisk(context: Context): EncryptedSeed? = loadSeedFromDir(getDatadir(context), SEED_FILE)
+ fun loadSeedFromDisk(context: Context): EncryptedSeed.V2.NoAuth? = loadSeedFromDir(getDatadir(context), SEED_FILE)
fun getSeedState(context: Context): SeedFileState = try {
when (val seed = loadSeedFromDisk(context)) {
@@ -56,7 +56,7 @@ object SeedManager {
}
/** Extract an encrypted seed contained in a given file/folder. */
- private fun loadSeedFromDir(dir: File, seedFileName: String): EncryptedSeed? {
+ private fun loadSeedFromDir(dir: File, seedFileName: String): EncryptedSeed.V2.NoAuth? {
val seedFile = File(dir, seedFileName)
return if (!seedFile.exists()) {
null
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/InflightPaymentsWatcher.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/InflightPaymentsWatcher.kt
index 39fa252e8..ef2439697 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/InflightPaymentsWatcher.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/InflightPaymentsWatcher.kt
@@ -128,7 +128,8 @@ class InflightPaymentsWatcher(context: Context, workerParams: WorkerParameters)
service.value = null
}
}
- Intent(applicationContext, NodeService::class.java).let { intent ->
+
+ Intent(applicationContext, application.nodeServiceClass).let { intent ->
applicationContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt
index c99dad40c..c9b5bf493 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt
@@ -2,33 +2,30 @@ package fr.acinq.phoenix.android.services
import android.app.Notification
import android.app.Service
+import android.content.Context
import android.content.Intent
-import android.content.pm.ServiceInfo
import android.os.Binder
import android.os.Build
-import android.os.Handler
import android.os.IBinder
-import android.os.Looper
import android.text.format.DateUtils
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.work.WorkManager
-import com.google.android.gms.tasks.OnCompleteListener
-import com.google.firebase.messaging.FirebaseMessaging
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.LiquidityEvents
import fr.acinq.lightning.MilliSatoshi
-import fr.acinq.lightning.io.PaymentReceived
-import fr.acinq.lightning.utils.Connection
+import fr.acinq.lightning.PaymentEvents
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.PhoenixBusiness
import fr.acinq.phoenix.android.BuildConfig
import fr.acinq.phoenix.android.PhoenixApplication
import fr.acinq.phoenix.android.security.EncryptedSeed
-import fr.acinq.phoenix.android.security.SeedManager
import fr.acinq.phoenix.android.utils.LegacyMigrationHelper
import fr.acinq.phoenix.android.utils.SystemNotificationHelper
import fr.acinq.phoenix.android.utils.datastore.InternalDataRepository
@@ -44,30 +41,30 @@ import fr.acinq.phoenix.utils.MnemonicLanguage
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import java.util.concurrent.locks.ReentrantLock
import kotlin.time.Duration.Companion.hours
-class NodeService : Service() {
+abstract class NodeService : Service() {
- private val log = LoggerFactory.getLogger(this::class.java)
- private val serviceScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
+ internal val log = LoggerFactory.getLogger(this::class.java)
+
+ internal val serviceScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
lateinit var internalData: InternalDataRepository
private val binder = NodeBinder()
- /** True if the service is running headless (that is without a GUI). In that case we should show a notification */
- @Volatile
- private var isHeadless = true
+ var isHeadless by mutableStateOf(false)
- // Notifications
- private lateinit var notificationManager: NotificationManagerCompat
+ lateinit var notificationManager: NotificationManagerCompat
/** State of the wallet, provides access to the business when started. Private so that it's not mutated from the outside. */
private val _state = MutableLiveData(NodeServiceState.Off)
@@ -77,7 +74,7 @@ class NodeService : Service() {
private val stateLock = ReentrantLock()
/** List of payments received while the app is in the background */
- private val receivedInBackground = mutableStateListOf()
+ val receivedInBackground = mutableStateListOf()
// jobs monitoring events/payments after business start
private var monitorPaymentsJob: Job? = null
@@ -87,112 +84,70 @@ class NodeService : Service() {
override fun onCreate() {
super.onCreate()
- log.debug("creating node service...")
+ log.info("creating node service...")
internalData = (applicationContext as PhoenixApplication).internalDataRepository
notificationManager = NotificationManagerCompat.from(this)
refreshFcmToken()
log.debug("service created")
}
- internal fun refreshFcmToken() {
- FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
- if (!task.isSuccessful) {
- log.warn("fetching FCM registration token failed: ${task.exception?.localizedMessage}")
- return@OnCompleteListener
- }
- task.result?.let { serviceScope.launch { internalData.saveFcmToken(it) } }
- })
- }
+ abstract fun refreshFcmToken()
+ abstract fun deleteFcmToken()
+
+ abstract fun isFcmAvailable(context: Context): Boolean
+
+ abstract suspend fun monitorFcmToken(business: PhoenixBusiness)
// =========================================================== //
// SERVICE LIFECYCLE //
// =========================================================== //
override fun onBind(intent: Intent?): IBinder {
- log.debug("binding node service from intent=$intent")
- // UI is binding to the service. The service is not headless anymore and we can remove the notification.
- isHeadless = false
+ log.debug("binding node service from intent={}", intent)
receivedInBackground.clear()
- stopForeground(STOP_FOREGROUND_REMOVE)
- notificationManager.cancel(SystemNotificationHelper.HEADLESS_NOTIF_ID)
return binder
}
/** When unbound, the service is running headless. */
override fun onUnbind(intent: Intent?): Boolean {
- isHeadless = true
return false
}
- private val shutdownHandler = Handler(Looper.getMainLooper())
- private val shutdownRunnable: Runnable = Runnable {
- if (isHeadless) {
- log.debug("reached scheduled shutdown...")
- if (receivedInBackground.isEmpty()) {
- stopForeground(STOP_FOREGROUND_REMOVE)
- } else {
- stopForeground(STOP_FOREGROUND_DETACH)
- }
- shutdown()
- }
+ override fun onTimeout(startId: Int) {
+ super.onTimeout(startId)
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ notificationManager.cancel(SystemNotificationHelper.HEADLESS_NOTIF_ID)
+ shutdown()
}
- /** Shutdown the node, close connections and stop the service */
- fun shutdown() {
- log.info("shutting down service in state=${_state.value?.name}")
- monitorNodeEventsJob?.cancel()
- stopSelf()
- _state.postValue(NodeServiceState.Off)
+ override fun onDestroy() {
+ super.onDestroy()
+ log.info("service destroyed")
}
// =========================================================== //
- // START COMMAND HANDLER //
+ // UTILITY //
// =========================================================== //
- /** Called when an intent is called for this service. */
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- super.onStartCommand(intent, flags, startId)
- log.info("start service from intent [ intent=$intent, flag=$flags, startId=$startId ]")
- val reason = intent?.getStringExtra(EXTRA_REASON)
-
- fun startForeground(notif: Notification) {
- if (Build.VERSION.SDK_INT >= 34) {
- ServiceCompat.startForeground(this, SystemNotificationHelper.HEADLESS_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)
- } else {
- startForeground(SystemNotificationHelper.HEADLESS_NOTIF_ID, notif)
- }
+ /** Shutdown the node, close connections and stop the service */
+ fun shutdown() {
+ log.info("shutting down service in state=${_state.value?.name}")
+ serviceScope.launch {
+ (application as? PhoenixApplication)?.business?.first()?.stop()
+ monitorNodeEventsJob?.cancel()
+ stopSelf()
+ _state.postValue(NodeServiceState.Off)
+ log.info("shutdown complete")
}
+ }
- val encryptedSeed = SeedManager.loadSeedFromDisk(applicationContext)
- when {
- _state.value is NodeServiceState.Running -> {
- // NOTE: the notification will NOT be shown if the app is already running
- val notif = SystemNotificationHelper.notifyRunningHeadless(applicationContext)
- startForeground(notif)
- }
- encryptedSeed is EncryptedSeed.V2.NoAuth -> {
- val seed = encryptedSeed.decrypt()
- log.debug("successfully decrypted seed in the background, starting wallet...")
- val notif = SystemNotificationHelper.notifyRunningHeadless(applicationContext)
- startBusiness(seed, requestCheckLegacyChannels = false)
- startForeground(notif)
- }
- else -> {
- log.warn("unhandled incoming payment with seed=${encryptedSeed?.name()} reason=$reason")
- val notif = when (reason) {
- "IncomingPayment" -> SystemNotificationHelper.notifyPaymentMissedAppUnavailable(applicationContext)
- "PendingSettlement" -> SystemNotificationHelper.notifyPendingSettlement(applicationContext)
- else -> SystemNotificationHelper.notifyRunningHeadless(applicationContext)
- }
- startForeground(notif)
- }
- }
- shutdownHandler.removeCallbacksAndMessages(null)
- shutdownHandler.postDelayed(shutdownRunnable, 2 * 60 * 1000L) // service will shutdown in 2 minutes
- if (!isHeadless) {
- stopForeground(STOP_FOREGROUND_REMOVE)
+ internal fun startForeground(notif: Notification, foregroundServiceType: Int) {
+ log.info("--- starting service in foreground with type=$foregroundServiceType ---")
+ if (Build.VERSION.SDK_INT >= 34) {
+ ServiceCompat.startForeground(this, SystemNotificationHelper.HEADLESS_NOTIF_ID, notif, foregroundServiceType)
+ } else {
+ startForeground(SystemNotificationHelper.HEADLESS_NOTIF_ID, notif)
}
- return START_NOT_STICKY
}
// =========================================================== //
@@ -216,10 +171,6 @@ class NodeService : Service() {
serviceScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e ->
log.error("error when starting node: ", e)
_state.postValue(NodeServiceState.Error(e))
- if (isHeadless) {
- shutdown()
- stopForeground(STOP_FOREGROUND_REMOVE)
- }
}) {
log.info("cancel competing workers")
val wm = WorkManager.getInstance(applicationContext)
@@ -256,7 +207,7 @@ class NodeService : Service() {
val trustedSwapInTxs = LegacyPrefsDatastore.getMigrationTrustedSwapInTxs(applicationContext).first()
val preferredFiatCurrency = userPrefs.getFiatCurrency.first()
- monitorPaymentsJob = serviceScope.launch { monitorPaymentsWhenHeadless(business.peerManager, business.currencyManager, userPrefs) }
+ monitorPaymentsJob = serviceScope.launch { monitorPaymentsWhenHeadless(business.nodeParamsManager, business.currencyManager, userPrefs) }
monitorNodeEventsJob = serviceScope.launch { monitorNodeEvents(business.peerManager, business.nodeParamsManager) }
monitorFcmTokenJob = serviceScope.launch { monitorFcmToken(business) }
monitorInFlightPaymentsJob = serviceScope.launch { monitorInFlightPayments(business.peerManager) }
@@ -285,21 +236,14 @@ class NodeService : Service() {
}
}
- private suspend fun monitorFcmToken(business: PhoenixBusiness) {
- val token = internalData.getFcmToken.filterNotNull().first()
- business.connectionsManager.connections.first { it.peer == Connection.ESTABLISHED }
- business.registerFcmToken(token)
- }
-
private suspend fun monitorNodeEvents(peerManager: PeerManager, nodeParamsManager: NodeParamsManager) {
val monitoringStartedAt = currentTimestampMillis()
combine(peerManager.swapInNextTimeout, nodeParamsManager.nodeParams.filterNotNull().first().nodeEvents) { nextTimeout, nodeEvent ->
nextTimeout to nodeEvent
}.collect { (nextTimeout, event) ->
- // TODO: click on notif must deeplink to the notification screen
when (event) {
is LiquidityEvents.Rejected -> {
- log.debug("processing liquidity_event=$event")
+ log.debug("processing liquidity_event={}", event)
if (event.source == LiquidityEvents.Source.OnChainWallet) {
// Check the last time a rejected on-chain swap notification has been shown. If recent, we do not want to trigger a notification every time.
val lastRejectedSwap = internalData.getLastRejectedOnchainSwap.first().takeIf {
@@ -336,19 +280,20 @@ class NodeService : Service() {
}
}
- private suspend fun monitorPaymentsWhenHeadless(peerManager: PeerManager, currencyManager: CurrencyManager, userPrefs: UserPrefsRepository) {
- peerManager.getPeer().eventsFlow.collect { event ->
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private suspend fun monitorPaymentsWhenHeadless(nodeParamsManager: NodeParamsManager, currencyManager: CurrencyManager, userPrefs: UserPrefsRepository) {
+ nodeParamsManager.nodeParams.filterNotNull().flatMapLatest { it.nodeEvents }.collect { event ->
when (event) {
- is PaymentReceived -> {
+ is PaymentEvents.PaymentReceived -> {
if (isHeadless) {
- receivedInBackground.add(event.received.amount)
+ receivedInBackground.add(event.amount)
SystemNotificationHelper.notifyPaymentsReceived(
context = applicationContext,
userPrefs = userPrefs,
- paymentHash = event.incomingPayment.paymentHash,
- amount = event.received.amount,
+ paymentHash = event.paymentHash,
+ amount = event.amount,
rates = currencyManager.ratesFlow.value,
- isHeadless = isHeadless && receivedInBackground.size == 1
+ isFromBackground = isHeadless
)
}
}
@@ -375,5 +320,8 @@ class NodeService : Service() {
companion object {
const val EXTRA_REASON = "${BuildConfig.APPLICATION_ID}.SERVICE_SPAWN_REASON"
+ const val EXTRA_ORIGIN = "${BuildConfig.APPLICATION_ID}.SERVICE_SPAWN_ORIGIN"
+ const val ORIGIN_FCM = "fcm"
+ const val ORIGIN_HEADLESS = "fcm"
}
-}
\ No newline at end of file
+}
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeState.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeState.kt
index 82ac1b0ca..2195691e9 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeState.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeState.kt
@@ -29,11 +29,11 @@ sealed class NodeServiceState {
val name: String by lazy { this.javaClass.simpleName }
/** Default state, the node is not started. */
- object Off : NodeServiceState()
+ data object Off : NodeServiceState()
/** This is an utility state that is used when the binding between the service holding the state and the consumers of that state is disconnected. */
- object Disconnected : NodeServiceState()
- object Init : NodeServiceState()
- object Running : NodeServiceState()
+ data object Disconnected : NodeServiceState()
+ data object Init : NodeServiceState()
+ data object Running : NodeServiceState()
data class Error(val cause: Throwable) : NodeServiceState()
}
\ No newline at end of file
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ExperimentalView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ExperimentalView.kt
index fbc8f7a3f..45eb4a4ce 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ExperimentalView.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ExperimentalView.kt
@@ -35,6 +35,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.viewModel
+import fr.acinq.phoenix.android.AppViewModel
import fr.acinq.phoenix.android.PhoenixApplication
import fr.acinq.phoenix.android.R
import fr.acinq.phoenix.android.business
@@ -118,6 +119,7 @@ class ExperimentalViewModel(val peerManager: PeerManager, val internalDataReposi
@Composable
fun ExperimentalView(
onBackClick: () -> Unit,
+ appViewModel: AppViewModel,
) {
val vm = viewModel(factory = ExperimentalViewModel.Factory(business.peerManager))
@@ -131,6 +133,9 @@ fun ExperimentalView(
Card(modifier = Modifier.fillMaxWidth()) {
ClaimAddressButton(state = vm.claimAddressState, onClaim = { vm.claimAddress() })
}
+
+ // flavored component
+ ManageHeadlessView(appViewModel = appViewModel)
}
}
@@ -156,7 +161,7 @@ private fun ClaimAddressButton(
leadingIcon = { PhoenixIcon(R.drawable.ic_arobase) },
subtitle = {
Text(text = stringResource(id = R.string.bip353_subtitle))
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(16.dp))
FilledButton(
text = stringResource(id = R.string.bip353_claim_button),
onClick = onClaim,
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ResetWallet.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ResetWallet.kt
index 19574ecd6..a28210429 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ResetWallet.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ResetWallet.kt
@@ -49,6 +49,7 @@ import fr.acinq.phoenix.android.LocalBitcoinUnit
import fr.acinq.phoenix.android.LocalFiatCurrency
import fr.acinq.phoenix.android.MainActivity
import fr.acinq.phoenix.android.R
+import fr.acinq.phoenix.android.application
import fr.acinq.phoenix.android.business
import fr.acinq.phoenix.android.components.BorderButton
import fr.acinq.phoenix.android.components.Button
@@ -328,6 +329,7 @@ private fun WalletDeleted() {
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
+ val application = application
SuccessMessage(header = stringResource(id = R.string.reset_wallet_success))
Spacer(modifier = Modifier.height(16.dp))
BorderButton(
@@ -335,7 +337,7 @@ private fun WalletDeleted() {
icon = R.drawable.ic_check,
onClick = {
context.startActivity(
- Intent(context, MainActivity::class.java).apply {
+ Intent(context, application.mainActivityClass).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
)
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt
index c4bd04a40..c8ce8a4a5 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt
@@ -37,6 +37,7 @@ import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.android.BuildConfig
import fr.acinq.phoenix.android.MainActivity
+import fr.acinq.phoenix.android.PhoenixApplication
import fr.acinq.phoenix.android.R
import fr.acinq.phoenix.android.utils.Converter.toAbsoluteDateString
import fr.acinq.phoenix.android.utils.Converter.toPrettyString
@@ -72,6 +73,10 @@ object SystemNotificationHelper {
/** If the remaining blocks count before a swap timeout is lower than this, we should mention it in the notification. */
private const val SWAP_TIMEOUT_THRESHOLD_IN_BLOCKS = 144 * 30 * 2 // ~2 months
+ private fun getActivityClass(context: Context): Class? {
+ return (context as? PhoenixApplication)?.mainActivityClass
+ }
+
fun registerNotificationChannels(context: Context) {
// notification channels (android 8+)
context.getSystemService(NotificationManager::class.java)?.createNotificationChannels(
@@ -98,15 +103,18 @@ object SystemNotificationHelper {
)
}
- fun notifyRunningHeadless(context: Context): Notification {
+ fun getHeadlessNotification(context: Context): Notification {
return NotificationCompat.Builder(context, HEADLESS_NOTIF_CHANNEL).apply {
setContentTitle(context.getString(R.string.notif_headless_title_default))
setSmallIcon(R.drawable.ic_phoenix_outline)
- }.build().also {
- if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
- NotificationManagerCompat.from(context).notify(HEADLESS_NOTIF_ID, it)
- }
- }
+ }.build()
+ }
+
+ fun getHeadlessFailureNotification(context: Context): Notification {
+ return NotificationCompat.Builder(context, HEADLESS_NOTIF_CHANNEL).apply {
+ setContentTitle(context.getString(R.string.notif_headless_title_failure))
+ setSmallIcon(R.drawable.ic_phoenix_outline)
+ }.build()
}
private fun notifyPaymentFailed(context: Context, title: String, message: String, deepLink: String?): Notification {
@@ -116,8 +124,8 @@ object SystemNotificationHelper {
setStyle(NotificationCompat.BigTextStyle().bigText(message))
setSmallIcon(R.drawable.ic_phoenix_outline)
val intent = deepLink?.let {
- Intent(Intent.ACTION_VIEW, it.toUri(), context, MainActivity::class.java)
- } ?: Intent(context, MainActivity::class.java)
+ Intent(Intent.ACTION_VIEW, it.toUri(), context, getActivityClass(context))
+ } ?: Intent(context, getActivityClass(context))
setContentIntent(
TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent)
@@ -218,7 +226,7 @@ object SystemNotificationHelper {
setContentText(context.getString(R.string.notif_pending_settlement_message))
setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notif_pending_settlement_message)))
setSmallIcon(R.drawable.ic_phoenix_outline)
- setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE))
+ setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, getActivityClass(context)), PendingIntent.FLAG_IMMUTABLE))
setAutoCancel(true)
}.build().also {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
@@ -233,7 +241,7 @@ object SystemNotificationHelper {
setContentText(context.getString(R.string.notif_inflight_payment_message))
setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notif_inflight_payment_message)))
setSmallIcon(R.drawable.ic_phoenix_outline)
- setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE))
+ setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, getActivityClass(context)), PendingIntent.FLAG_IMMUTABLE))
setAutoCancel(true)
}.build().also {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
@@ -248,7 +256,7 @@ object SystemNotificationHelper {
paymentHash: ByteVector32,
amount: MilliSatoshi,
rates: List,
- isHeadless: Boolean,
+ isFromBackground: Boolean,
): Notification {
val isFiat = userPrefs.getIsAmountInFiat.first() && rates.isNotEmpty()
val unit = if (isFiat) {
@@ -277,14 +285,14 @@ object SystemNotificationHelper {
return NotificationCompat.Builder(context, PAYMENT_RECEIVED_NOTIF_CHANNEL).apply {
setContentTitle(context.getString(R.string.notif_headless_received, amount.toPrettyString(unit, rate, withUnit = true)))
setSmallIcon(R.drawable.ic_phoenix_outline)
- val intent = Intent(Intent.ACTION_VIEW,"phoenix:payments/${WalletPaymentId.DbType.INCOMING.value}/${paymentHash.toHex()}".toUri(), context, MainActivity::class.java).apply {
+ val intent = Intent(Intent.ACTION_VIEW,"phoenix:payments/${WalletPaymentId.DbType.INCOMING.value}/${paymentHash.toHex()}".toUri(), context, getActivityClass(context)).apply {
Intent.FLAG_ACTIVITY_SINGLE_TOP
}
setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT))
setAutoCancel(true)
}.build().also {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
- NotificationManagerCompat.from(context).notify(if (isHeadless) HEADLESS_NOTIF_ID else Random().nextInt(), it)
+ NotificationManagerCompat.from(context).notify(if (isFromBackground) HEADLESS_NOTIF_ID else Random().nextInt(), it)
}
}
}
@@ -294,7 +302,7 @@ object SystemNotificationHelper {
setContentTitle(context.getString(R.string.notif_watcher_revoked_commit_title))
setContentText(context.getString(R.string.notif_watcher_revoked_commit_message))
setSmallIcon(R.drawable.ic_phoenix_outline)
- setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE))
+ setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, getActivityClass(context)), PendingIntent.FLAG_IMMUTABLE))
setAutoCancel(true)
}.let {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/InternalDataRepository.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/InternalDataRepository.kt
index 759923057..88168ef9d 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/InternalDataRepository.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/InternalDataRepository.kt
@@ -57,6 +57,8 @@ class InternalDataRepository(private val internalData: DataStore) {
private val SHOW_SPLICEOUT_CAPACITY_DISCLAIMER = booleanPreferencesKey("SHOW_SPLICEOUT_CAPACITY_DISCLAIMER")
private val REMOTE_WALLET_NOTICE_READ_INDEX = intPreferencesKey("REMOTE_WALLET_NOTICE_READ_INDEX")
private val BIP_353_ADDRESS = stringPreferencesKey("BIP_353_ADDRESS")
+ private val BACKGROUND_SERVICE_MODE = booleanPreferencesKey("BACKGROUND_SERVICE_MODE")
+ private val BACKGROUND_SERVICE_MODE_WITH_WAKELOCK = booleanPreferencesKey("BACKGROUND_SERVICE_MODE_WITH_WAKELOCK")
}
val log = LoggerFactory.getLogger(this::class.java)
@@ -147,4 +149,22 @@ class InternalDataRepository(private val internalData: DataStore) {
val getBip353Address: Flow = safeData.map { it[BIP_353_ADDRESS] ?: "" }
suspend fun saveBip353Address(address: String) = internalData.edit { it[BIP_353_ADDRESS] = address }
+
+ val getBackgroundServiceMode: Flow?> = safeData.map {
+ val isEnabled = it[BACKGROUND_SERVICE_MODE]
+ val withWakeLock = it[BACKGROUND_SERVICE_MODE_WITH_WAKELOCK] ?: false
+ isEnabled?.let { it to withWakeLock }
+ }
+ suspend fun saveBackgroundServiceModeEnabled(withWakeLock: Boolean) {
+ internalData.edit {
+ it[BACKGROUND_SERVICE_MODE] = true
+ it[BACKGROUND_SERVICE_MODE_WITH_WAKELOCK] = withWakeLock
+ }
+ }
+ suspend fun saveBackgroundServiceModeDisabled() {
+ internalData.edit {
+ it[BACKGROUND_SERVICE_MODE] = false
+ it[BACKGROUND_SERVICE_MODE_WITH_WAKELOCK] = false
+ }
+ }
}
\ No newline at end of file
diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions.kt
index d5ea31466..00d74f166 100644
--- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions.kt
+++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions.kt
@@ -17,22 +17,18 @@
package fr.acinq.phoenix.android.utils
import android.content.*
-import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.core.content.FileProvider
import fr.acinq.lightning.db.*
import fr.acinq.lightning.utils.Connection
-import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.android.*
import fr.acinq.phoenix.android.R
import fr.acinq.phoenix.android.utils.Converter.toPrettyString
import fr.acinq.phoenix.data.BitcoinUnit
import fr.acinq.phoenix.data.FiatCurrency
import fr.acinq.phoenix.utils.extensions.desc
-import java.io.File
import java.security.cert.CertificateException
import java.util.*
import kotlin.contracts.ExperimentalContracts
diff --git a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml
index 5e546b468..a7becc707 100644
--- a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml
+++ b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml
@@ -80,8 +80,7 @@
Ver detalles
Procesamiento en segundo plano restringido.
- Phoenix no podrá recibir pagos cuando esté en segundo plano, o cuando esté cerrado.
- Esto ocurre porque:
+ Phoenix podría no recibir pagos cuando esté en segundo plano, o cuando esté cerrado.
Tor está activado
El dispositivo está en modo de ahorro de energía
FCM notificaciones no disponibles
diff --git a/phoenix-android/src/main/res/values-cs/important_strings.xml b/phoenix-android/src/main/res/values-cs/important_strings.xml
index 9f4a465c7..c6bd3452b 100644
--- a/phoenix-android/src/main/res/values-cs/important_strings.xml
+++ b/phoenix-android/src/main/res/values-cs/important_strings.xml
@@ -83,8 +83,7 @@
Klikněte pro podrobnosti
Omezeno zpracovávání na pozadí
- Phoenix nebude moci přijímat platby, pokud je na pozadí nebo pokud je zavřený..
- Děje se tak, protože:
+ Phoenix možná nebude moci přijímat platby, pokud je na pozadí nebo pokud je zavřený..
Tor je povolen
Zařízení je v úsporném režimu
Oznámení FCM nejsou k dispozici
diff --git a/phoenix-android/src/main/res/values-de/important_strings.xml b/phoenix-android/src/main/res/values-de/important_strings.xml
index 08c413c06..479b4dc7f 100644
--- a/phoenix-android/src/main/res/values-de/important_strings.xml
+++ b/phoenix-android/src/main/res/values-de/important_strings.xml
@@ -80,8 +80,7 @@
Tippen Sie für Details
Hintergrundverarbeitung eingeschränkt
- Phoenix kann keine Zahlungen empfangen, wenn es im Hintergrund läuft oder geschlossen ist.
- Dies geschieht, weil:
+ Phoenix wird möglicherweise keine Zahlungen erhalten können, wenn es im Hintergrund läuft oder geschlossen ist.
Tor ist aktiviert
Das Gerät ist im Energiesparmodus
FCM-Benachrichtigungen nicht verfügbar
diff --git a/phoenix-android/src/main/res/values-es/important_strings.xml b/phoenix-android/src/main/res/values-es/important_strings.xml
index b77e6643a..ff49b79a9 100644
--- a/phoenix-android/src/main/res/values-es/important_strings.xml
+++ b/phoenix-android/src/main/res/values-es/important_strings.xml
@@ -75,8 +75,7 @@
Ver detalles
Procesamiento en segundo plano restringido.
- Phoenix no podrá recibir pagos cuando esté en segundo plano, o cuando esté cerrado.
- Esto ocurre porque:
+ Phoenix podría no recibir pagos cuando esté en segundo plano, o cuando esté cerrado.
Tor está activado
El dispositivo está en modo de ahorro de energía
FCM notificaciones no disponibles
diff --git a/phoenix-android/src/main/res/values-fr/important_strings.xml b/phoenix-android/src/main/res/values-fr/important_strings.xml
index 52c827bbe..7c95f722f 100644
--- a/phoenix-android/src/main/res/values-fr/important_strings.xml
+++ b/phoenix-android/src/main/res/values-fr/important_strings.xml
@@ -83,8 +83,7 @@
Voir les détails
Fonctionnement en arrière plan restreint
- Phoenix ne pourra pas recevoir de paiements quand l\'application est en arrière plan ou est fermée.
- Cela vient du fait que:
+ Phoenix pourrait ne pas être en mesure de recevoir de paiements quand l\'application est en arrière plan ou est fermée.
Tor est activé
Le téléphone est en mode économie d\'énergie
Les notifications FCM sont indisponibles
diff --git a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml
index 75c5c5ed4..6d033c98d 100644
--- a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml
+++ b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml
@@ -83,8 +83,7 @@
Toque para obter detalhes
Processamento em segundo plano restrito
- O Phoenix não poderá receber pagamentos quando estiver em segundo plano ou quando estiver fechado.
- Isso acontece porque:
+ A Phoenix pode não conseguir receber pagamentos quando estiver em segundo plano ou quando estiver fechado.
Tor está ativado
O dispositivo está no modo de economia de energia
FCM notificações indisponíveis
diff --git a/phoenix-android/src/main/res/values-sk/important_strings.xml b/phoenix-android/src/main/res/values-sk/important_strings.xml
index 0b61efaa3..954ca880b 100644
--- a/phoenix-android/src/main/res/values-sk/important_strings.xml
+++ b/phoenix-android/src/main/res/values-sk/important_strings.xml
@@ -83,8 +83,7 @@
Kliknite pre podrobnosti
Omedzené spracovanie pozadia
- Fénix nebude môcť prijímať platby, keď je na pozadí alebo keď je zatvorený.
- Deje sa tak, pretože:
+ Phoenix možno nebude môcť prijímať platby, keď je na pozadí alebo keď je zatvorený.
Tor je povolený
Zariadenie je v úspornom režime
Oznamy FCM nie sú k dispozícii
diff --git a/phoenix-android/src/main/res/values-sw/important_strings.xml b/phoenix-android/src/main/res/values-sw/important_strings.xml
index 16b45bbdc..4a55d1a13 100644
--- a/phoenix-android/src/main/res/values-sw/important_strings.xml
+++ b/phoenix-android/src/main/res/values-sw/important_strings.xml
@@ -87,8 +87,7 @@
Bonyeza kwa maelezo
Usindikaji wa nyuma umepunguzwa
- Phoenix haitaweza kupokea malipo wakati iko nyuma, au wakati imefungwa.
- Hii hutokea kwa sababu:
+ Huenda Phoenix isiweze kupokea malipo ikiwa chinichini, au inapofungwa.
Tor imewashwa
Kifaa kipo kwenye hali ya kuokoa nguvu
Arifa za FCM hazipatikani
diff --git a/phoenix-android/src/main/res/values-vi/important_strings.xml b/phoenix-android/src/main/res/values-vi/important_strings.xml
index a8a7d5224..8a7cdd661 100644
--- a/phoenix-android/src/main/res/values-vi/important_strings.xml
+++ b/phoenix-android/src/main/res/values-vi/important_strings.xml
@@ -90,8 +90,7 @@
Nhấn để biết thêm chi tiết
Hạn chế xử lý nền
- Phoenix sẽ không thể nhận thanh toán khi nó ở chế độ nền hoặc khi nó bị đóng.
- Điều này xảy ra vì:
+ Phoenix có thể không nhận được thanh toán khi đang ở chế độ nền hoặc khi đóng cửa.
Tor đã được bật
Thiết bị đang ở chế độ tiết kiệm năng lượng
Thông báo FCM không khả dụng
diff --git a/phoenix-android/src/main/res/values/important_strings.xml b/phoenix-android/src/main/res/values/important_strings.xml
index a67f3efc9..fcf7b958d 100644
--- a/phoenix-android/src/main/res/values/important_strings.xml
+++ b/phoenix-android/src/main/res/values/important_strings.xml
@@ -29,6 +29,7 @@
Phoenix is running in the background
+ Background start failed
Received +%1$s
Please start Phoenix
@@ -87,8 +88,7 @@
Tap for details
Background processing restricted
- Phoenix will not be able to receive payments when it is in the background, or when it is closed.
- This happens because:
+ Phoenix may not be able to receive payments when it is in the background, or when it is closed.
Tor is enabled
The device is in power saving mode
FCM notifications unavailable
diff --git a/phoenix-android/src/main/res/values/strings.xml b/phoenix-android/src/main/res/values/strings.xml
index 8b4251ed2..71eb8a341 100644
--- a/phoenix-android/src/main/res/values/strings.xml
+++ b/phoenix-android/src/main/res/values/strings.xml
@@ -235,6 +235,7 @@
Share
OK
Save
+ Start
Confirm
Cancel
Close