From a11784c615fce98fbda72a13508a9894a9f3b5f0 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Sat, 26 Mar 2022 16:49:08 +0000
Subject: [PATCH] android: foreground service to receive messages (#454)
* android: foreground service to receive messages
* android: fix duplicate chat (caused by persistent state of the service)
* option to turn off background service
* fix: foreground service failing to start when the new user is created
* remove unused background manager
---
apps/android/app/src/main/AndroidManifest.xml | 33 ++-
.../java/chat/simplex/app/MainActivity.kt | 22 ++
.../main/java/chat/simplex/app/SimplexApp.kt | 52 ++--
.../java/chat/simplex/app/SimplexService.kt | 242 ++++++++++++++++++
.../java/chat/simplex/app/model/BGManager.kt | 44 ----
.../java/chat/simplex/app/model/ChatModel.kt | 1 +
.../java/chat/simplex/app/model/SimpleXAPI.kt | 42 ++-
.../chat/simplex/app/views/WelcomeView.kt | 2 +
.../app/views/usersettings/SettingsView.kt | 46 +++-
.../app/src/main/res/values/strings.xml | 6 +-
10 files changed, 410 insertions(+), 80 deletions(-)
create mode 100644 apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt
delete mode 100644 apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt
diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml
index 284544089a..17c4303b20 100644
--- a/apps/android/app/src/main/AndroidManifest.xml
+++ b/apps/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,10 @@
+
+
+
+
@@ -16,6 +20,8 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.SimpleX">
+
+
+
@@ -34,6 +41,7 @@
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt
index 18288bc4f1..c4b44eff13 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt
@@ -16,6 +16,7 @@ import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.AndroidViewModel
+import androidx.work.*
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.NtfManager
import chat.simplex.app.ui.theme.SimpleXTheme
@@ -27,11 +28,13 @@ import chat.simplex.app.views.chatlist.openChat
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.newchat.*
+import java.util.concurrent.TimeUnit
//import kotlinx.serialization.decodeFromString
class MainActivity: ComponentActivity() {
private val vm by viewModels()
+ private val chatController by lazy { (application as SimplexApp).chatController }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -49,6 +52,25 @@ class MainActivity: ComponentActivity() {
}
}
}
+ schedulePeriodicServiceRestartWorker()
+ }
+
+ private fun schedulePeriodicServiceRestartWorker() {
+ val workerVersion = chatController.getAutoRestartWorkerVersion()
+ val workPolicy = if (workerVersion == SimplexService.SERVICE_START_WORKER_VERSION) {
+ Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy")
+ ExistingPeriodicWorkPolicy.KEEP
+ } else {
+ Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy")
+ chatController.setAutoRestartWorkerVersion(SimplexService.SERVICE_START_WORKER_VERSION)
+ ExistingPeriodicWorkPolicy.REPLACE
+ }
+ val work = PeriodicWorkRequestBuilder(SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
+ .addTag(SimplexService.TAG)
+ .addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
+ .build()
+ Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes")
+ WorkManager.getInstance(this)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
}
}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt
index a8f0ada2be..14012c5ef7 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt
@@ -1,8 +1,11 @@
package chat.simplex.app
import android.app.Application
+import android.content.Context
+import android.content.SharedPreferences
import android.net.LocalServerSocket
import android.util.Log
+import androidx.lifecycle.*
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.withApi
import java.io.BufferedReader
@@ -24,32 +27,43 @@ external fun chatInit(path: String): ChatCtrl
external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String
external fun chatRecvMsg(ctrl: ChatCtrl) : String
-//class SimplexApp: Application(), LifecycleEventObserver {
-class SimplexApp: Application() {
- private lateinit var controller: ChatController
- lateinit var chatModel: ChatModel
- private lateinit var ntfManager: NtfManager
+class SimplexApp: Application(), LifecycleEventObserver {
+ val chatController: ChatController by lazy {
+ val ctrl = chatInit(applicationContext.filesDir.toString())
+ ChatController(ctrl, ntfManager, applicationContext)
+ }
+
+ val chatModel: ChatModel by lazy {
+ chatController.chatModel
+ }
+
+ private val ntfManager: NtfManager by lazy {
+ NtfManager(applicationContext)
+ }
override fun onCreate() {
super.onCreate()
-// ProcessLifecycleOwner.get().lifecycle.addObserver(this)
- ntfManager = NtfManager(applicationContext)
- val ctrl = chatInit(applicationContext.filesDir.toString())
- controller = ChatController(ctrl, ntfManager, applicationContext)
- chatModel = controller.chatModel
+ ProcessLifecycleOwner.get().lifecycle.addObserver(this)
withApi {
- val user = controller.apiGetActiveUser()
- if (user != null) controller.startChat(user)
+ val user = chatController.apiGetActiveUser()
+ if (user != null) {
+ chatController.startChat(user)
+ SimplexService.start(applicationContext)
+ }
}
}
-// override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
-// Log.d(TAG, "onStateChanged: $event")
-// if (event == Lifecycle.Event.ON_STOP) {
-// Log.e(TAG, "BGManager schedule ${Clock.System.now()}")
-// BGManager.schedule(applicationContext)
-// }
-// }
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ Log.d(TAG, "onStateChanged: $event")
+ withApi {
+ when (event) {
+ Lifecycle.Event.ON_STOP ->
+ if (!chatController.getRunServiceInBackground()) SimplexService.stop(applicationContext)
+ Lifecycle.Event.ON_START ->
+ SimplexService.start(applicationContext)
+ }
+ }
+ }
companion object {
init {
diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt
new file mode 100644
index 0000000000..34907f9a48
--- /dev/null
+++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt
@@ -0,0 +1,242 @@
+package chat.simplex.app
+
+import android.app.*
+import android.content.*
+import android.os.*
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
+import androidx.work.*
+import chat.simplex.app.views.helpers.withApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+// based on:
+// https://robertohuertas.com/2019/06/29/android_foreground_services/
+// https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
+
+class SimplexService: Service() {
+ private var wakeLock: PowerManager.WakeLock? = null
+ private var isServiceStarted = false
+ private var isStartingService = false
+ private var notificationManager: NotificationManager? = null
+ private var serviceNotification: Notification? = null
+ private val chatController by lazy { (application as SimplexApp).chatController }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(TAG, "onStartCommand startId: $startId")
+ if (intent != null) {
+ val action = intent.action
+ Log.d(TAG, "intent action $action")
+ when (action) {
+ Action.START.name -> startService()
+ Action.STOP.name -> stopService()
+ else -> Log.e(TAG, "No action in the intent")
+ }
+ } else {
+ Log.d(TAG, "null intent. Probably restarted by the system.")
+ }
+ return START_STICKY // to restart if killed
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d(TAG, "Simplex service created")
+ val title = getString(R.string.simplex_service_notification_title)
+ val text = getString(R.string.simplex_service_notification_text)
+ notificationManager = createNotificationChannel()
+ serviceNotification = createNotification(title, text)
+
+ startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "Simplex service destroyed")
+ stopService()
+ sendBroadcast(Intent(this, AutoRestartReceiver::class.java)) // Restart if necessary!
+ super.onDestroy()
+ }
+
+ private fun startService() {
+ Log.d(TAG, "SimplexService startService")
+ if (isServiceStarted || isStartingService) return
+ val self = this
+ isStartingService = true
+ withApi {
+ try {
+ val user = chatController.apiGetActiveUser()
+ if (user != null) {
+ Log.w(TAG, "Starting foreground service")
+ chatController.startChat(user)
+ chatController.startReceiver()
+ isServiceStarted = true
+ saveServiceState(self, ServiceState.STARTED)
+ wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
+ newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
+ acquire()
+ }
+ }
+ }
+ } finally {
+ isStartingService = false
+ }
+ }
+ }
+
+ private fun stopService() {
+ Log.d(TAG, "Stopping foreground service")
+ try {
+ wakeLock?.let {
+ while (it.isHeld) it.release() // release all, in case acquired more than once
+ }
+ wakeLock = null
+ stopForeground(true)
+ stopSelf()
+ } catch (e: Exception) {
+ Log.d(TAG, "Service stopped without being started: ${e.message}")
+ }
+
+ isServiceStarted = false
+ saveServiceState(this, ServiceState.STOPPED)
+ }
+
+ private fun createNotificationChannel(): NotificationManager? {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW).let {
+ it.setShowBadge(false) // no long-press badge
+ it
+ }
+ notificationManager.createNotificationChannel(channel)
+ return notificationManager
+ }
+ return null
+ }
+
+ private fun createNotification(title: String, text: String): Notification {
+ val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent ->
+ PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
+ }
+ return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ntf_icon)
+ .setColor(0x88FFFF)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(pendingIntent)
+ .setSound(null)
+ .setShowWhen(false) // no date/time
+ .build()
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null // no binding
+ }
+
+ // re-schedules the task when "Clear recent apps" is pressed
+ override fun onTaskRemoved(rootIntent: Intent) {
+ val restartServiceIntent = Intent(applicationContext, SimplexService::class.java).also {
+ it.setPackage(packageName)
+ };
+ val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
+ applicationContext.getSystemService(Context.ALARM_SERVICE);
+ val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
+ alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
+ }
+
+ // restart on reboot
+ class StartReceiver: BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "StartReceiver: onReceive called")
+ scheduleStart(context)
+ }
+ }
+
+ // restart on destruction
+ class AutoRestartReceiver: BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "AutoRestartReceiver: onReceive called")
+ scheduleStart(context)
+ }
+ }
+
+ class ServiceStartWorker(private val context: Context, params: WorkerParameters): CoroutineWorker(context, params) {
+ override suspend fun doWork(): Result {
+ val id = this.id
+ if (context.applicationContext !is Application) {
+ Log.d(TAG, "ServiceStartWorker: Failed, no application found (work ID: $id)")
+ return Result.failure()
+ }
+ if (getServiceState(context) == ServiceState.STARTED) {
+ Log.d(TAG, "ServiceStartWorker: Starting foreground service (work ID: $id)")
+ start(context)
+ }
+ return Result.success()
+ }
+ }
+
+ enum class Action {
+ START,
+ STOP
+ }
+
+ enum class ServiceState {
+ STARTED,
+ STOPPED,
+ }
+
+ companion object {
+ const val TAG = "SIMPLEX_SERVICE"
+ const val NOTIFICATION_CHANNEL_ID = "chat.simplex.app.SIMPLEX_SERVICE_NOTIFICATION"
+ const val NOTIFICATION_CHANNEL_NAME = "SimpleX Chat service"
+ const val SIMPLEX_SERVICE_ID = 6789
+ const val SERVICE_START_WORKER_VERSION = BuildConfig.VERSION_CODE
+ const val SERVICE_START_WORKER_INTERVAL_MINUTES = 3 * 60L
+ const val SERVICE_START_WORKER_WORK_NAME_PERIODIC = "SimplexAutoRestartWorkerPeriodic" // Do not change!
+
+ private const val WAKE_LOCK_TAG = "SimplexService::lock"
+ private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_SERVICE_PREFS"
+ private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
+ private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
+
+ fun scheduleStart(context: Context) {
+ Log.d(TAG, "Enqueuing work to start subscriber service")
+ val workManager = WorkManager.getInstance(context)
+ val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
+ workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
+ }
+
+ suspend fun start(context: Context) = serviceAction(context, Action.START)
+
+ suspend fun stop(context: Context) = serviceAction(context, Action.STOP)
+
+ private suspend fun serviceAction(context: Context, action: Action) {
+ Log.d(TAG, "SimplexService serviceAction: ${action.name}")
+ withContext(Dispatchers.IO) {
+ Intent(context, SimplexService::class.java).also {
+ it.action = action.name
+ ContextCompat.startForegroundService(context, it)
+ }
+ }
+ }
+
+ fun restart(context: Context) {
+ Intent(context, SimplexService::class.java).also { intent ->
+ context.stopService(intent) // Service will auto-restart
+ }
+ }
+
+ fun saveServiceState(context: Context, state: ServiceState) {
+ getPreferences(context).edit()
+ .putString(SHARED_PREFS_SERVICE_STATE, state.name)
+ .apply()
+ }
+
+ fun getServiceState(context: Context): ServiceState {
+ val value = getPreferences(context)
+ .getString(SHARED_PREFS_SERVICE_STATE, ServiceState.STOPPED.name)
+ return ServiceState.valueOf(value!!)
+ }
+
+ private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
+ }
+}
\ No newline at end of file
diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt
deleted file mode 100644
index 9894713752..0000000000
--- a/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package chat.simplex.app.model
-
-import android.content.Context
-import android.util.Log
-import androidx.work.*
-import chat.simplex.app.TAG
-import kotlinx.datetime.Clock
-import java.time.Duration
-
-class BGManager(appContext: Context, workerParams: WorkerParameters): //, ctrl: ChatCtrl):
- Worker(appContext, workerParams) {
-// val controller = ctrl
-
- init {}
-
- override fun doWork(): Result {
- Log.e(TAG, "BGManager doWork ${Clock.System.now()}")
- schedule(applicationContext)
- getNewItems()
- return Result.success()
- }
-
- private fun getNewItems() {
- Log.e(TAG, "BGManager getNewItems")
-// val json = chatRecvMsg(controller)
-// val r = APIResponse.decodeStr(json).resp
-// Log.d(TAG, "chatRecvMsg: ${r.responseType}")
- }
-
- companion object {
- val constraints = Constraints.Builder()
- .setRequiredNetworkType(NetworkType.CONNECTED)
- .build()
-
- fun schedule(appContext: Context) {
- val request = OneTimeWorkRequestBuilder()
- .setInitialDelay(Duration.ofMinutes(10))
- .setConstraints(constraints)
- .build()
- WorkManager.getInstance(appContext)
- .enqueue(request)
- }
- }
-}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
index 7bdf80eede..fd0ebfc580 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
@@ -29,6 +29,7 @@ class ChatModel(val controller: ChatController) {
var userSMPServers = mutableStateOf<(List)?>(null)
// set when app is opened via contact or invitation URI
var appOpenUrl = mutableStateOf(null)
+ var runServiceInBackground = mutableStateOf(true)
fun updateUserProfile(profile: Profile) {
val user = currentUser.value
diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
index c2ce6ad329..96a622c99c 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
@@ -3,6 +3,7 @@ package chat.simplex.app.model
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo
import android.content.Context
+import android.content.SharedPreferences
import android.util.Log
import androidx.compose.runtime.mutableStateOf
import chat.simplex.app.*
@@ -19,19 +20,25 @@ import kotlin.concurrent.thread
typealias ChatCtrl = Long
-open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context) {
+open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: NtfManager, val appContext: Context) {
var chatModel = ChatModel(this)
+ private val sharedPreferences: SharedPreferences = appContext.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
- suspend fun startChat(u: User) {
- Log.d(TAG, "user: $u")
+ init {
+ chatModel.runServiceInBackground.value = getRunServiceInBackground()
+ }
+
+ suspend fun startChat(user: User) {
+ Log.d(TAG, "user: $user")
try {
apiStartChat()
chatModel.userAddress.value = apiGetUserAddress()
chatModel.userSMPServers.value = getUserSMPServers()
- chatModel.chats.addAll(apiGetChats())
- chatModel.currentUser = mutableStateOf(u)
+ val chats = apiGetChats()
+ chatModel.chats.clear()
+ chatModel.chats.addAll(chats)
+ chatModel.currentUser = mutableStateOf(user)
chatModel.userCreated.value = true
- startReceiver()
Log.d(TAG, "started chat")
} catch(e: Error) {
Log.e(TAG, "failed starting chat $e")
@@ -40,6 +47,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
}
fun startReceiver() {
+ Log.d(TAG, "ChatController startReceiver")
thread(name="receiver") {
withApi { recvMspLoop() }
}
@@ -105,7 +113,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
suspend fun apiStartChat() {
val r = sendCmd(CC.StartChat())
- if (r is CR.ChatStarted ) return
+ if (r is CR.ChatStarted || r is CR.ChatRunning) return
throw Error("failed starting chat: ${r.responseType} ${r.details}")
}
@@ -359,6 +367,26 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
else e.string
chatModel.updateNetworkStatus(contact, Chat.NetworkStatus.Error(err))
}
+
+ fun getAutoRestartWorkerVersion(): Int = sharedPreferences.getInt(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
+
+ fun setAutoRestartWorkerVersion(version: Int) =
+ sharedPreferences.edit()
+ .putInt(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, version)
+ .apply()
+
+ fun getRunServiceInBackground(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true)
+
+ fun setRunServiceInBackground(runService: Boolean) =
+ sharedPreferences.edit()
+ .putBoolean(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, runService)
+ .apply()
+
+ companion object {
+ private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
+ private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
+ private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
+ }
}
enum class MsgDeleteMode(val mode: String) {
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt
index 3c2e33adbc..b39cc64b8c 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
+import chat.simplex.app.SimplexService
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Profile
import chat.simplex.app.views.helpers.withApi
@@ -151,6 +152,7 @@ fun CreateProfilePanel(chatModel: ChatModel) {
Profile(displayName, fullName, null)
)
chatModel.controller.startChat(user)
+ SimplexService.start(chatModel.controller.appContext)
}
},
enabled = (displayName.isNotEmpty() && isValidDisplayName(displayName))
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
index 581c9cf271..086a90f222 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
@@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
@@ -21,6 +21,7 @@ import chat.simplex.app.BuildConfig
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Profile
+import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.TerminalView
import chat.simplex.app.views.helpers.ProfileImage
@@ -32,6 +33,11 @@ fun SettingsView(chatModel: ChatModel) {
if (user != null) {
SettingsLayout(
profile = user.profile,
+ runServiceInBackground = chatModel.runServiceInBackground,
+ setRunServiceInBackground = { on ->
+ chatModel.controller.setRunServiceInBackground(on)
+ chatModel.runServiceInBackground.value = on
+ },
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } }
@@ -45,6 +51,8 @@ val simplexTeamUri =
@Composable
fun SettingsLayout(
profile: Profile,
+ runServiceInBackground: MutableState,
+ setRunServiceInBackground: (Boolean) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showTerminal: () -> Unit
@@ -143,6 +151,26 @@ fun SettingsLayout(
Spacer(Modifier.padding(horizontal = 4.dp))
Text("SMP servers")
}
+ SettingsSectionView() {
+ Icon(
+ Icons.Outlined.Bolt,
+ contentDescription = "Instant notifications",
+ )
+ Spacer(Modifier.padding(horizontal = 4.dp))
+ Text("Instant notifications", Modifier
+ .padding(end = 24.dp)
+ .fillMaxWidth()
+ .weight(1F))
+ Switch(
+ checked = runServiceInBackground.value,
+ onCheckedChange = { setRunServiceInBackground(it) },
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = MaterialTheme.colors.primary,
+ uncheckedThumbColor = HighOrLowlight
+ ),
+ modifier = Modifier.padding(end = 8.dp)
+ )
+ }
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showTerminal) {
Icon(
@@ -169,7 +197,7 @@ fun SettingsLayout(
)
}
Divider(Modifier.padding(horizontal = 8.dp))
- SettingsSectionView(click = {}) {
+ SettingsSectionView() {
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
}
@@ -177,13 +205,13 @@ fun SettingsLayout(
}
@Composable
-fun SettingsSectionView(click: () -> Unit, height: Dp = 48.dp, content: (@Composable () -> Unit)) {
+fun SettingsSectionView(click: (() -> Unit)? = null, height: Dp = 48.dp, content: (@Composable () -> Unit)) {
+ val modifier = Modifier
+ .padding(start = 8.dp)
+ .fillMaxWidth()
+ .height(height)
Row(
- Modifier
- .padding(start = 8.dp)
- .fillMaxWidth()
- .clickable(onClick = click)
- .height(height),
+ if (click == null) modifier else modifier.clickable(onClick = click),
verticalAlignment = Alignment.CenterVertically
) {
content()
@@ -201,6 +229,8 @@ fun PreviewSettingsLayout() {
SimpleXTheme {
SettingsLayout(
profile = Profile.sampleData,
+ runServiceInBackground = remember { mutableStateOf(true) },
+ setRunServiceInBackground = {},
showModal = {{}},
showCustomModal = {{}},
showTerminal = {}
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index c8517623e9..228c03d113 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -1,3 +1,7 @@
SimpleX
-
\ No newline at end of file
+
+
+ SimpleX Chat service
+ Waiting for incoming messages
+