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 +