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
This commit is contained in:
committed by
GitHub
parent
262c999e5c
commit
a11784c615
@@ -5,6 +5,10 @@
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -16,6 +20,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SimpleX">
|
||||
|
||||
<!-- Main activity -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -27,6 +33,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- open simplex:/ connection URI -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -34,6 +41,7 @@
|
||||
<data android:scheme="simplex" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="chat.simplex.app.provider"
|
||||
@@ -43,6 +51,29 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- SimplexService foreground service -->
|
||||
<service
|
||||
android:name=".SimplexService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false">
|
||||
</service>
|
||||
|
||||
<!-- SimplexService restart on reboot -->
|
||||
<receiver
|
||||
android:name=".SimplexService$StartReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- SimplexService restart on destruction -->
|
||||
<receiver
|
||||
android:name=".SimplexService$AutoRestartReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -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<SimplexViewModel>()
|
||||
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.ServiceStartWorker>(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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<BGManager>()
|
||||
.setInitialDelay(Duration.ofMinutes(10))
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(appContext)
|
||||
.enqueue(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ class ChatModel(val controller: ChatController) {
|
||||
var userSMPServers = mutableStateOf<(List<String>)?>(null)
|
||||
// set when app is opened via contact or invitation URI
|
||||
var appOpenUrl = mutableStateOf<Uri?>(null)
|
||||
var runServiceInBackground = mutableStateOf(true)
|
||||
|
||||
fun updateUserProfile(profile: Profile) {
|
||||
val user = currentUser.value
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<Boolean>,
|
||||
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 = {}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<resources>
|
||||
<string name="app_name">SimpleX</string>
|
||||
</resources>
|
||||
|
||||
<!-- SimpleX Chat foreground Service -->
|
||||
<string name="simplex_service_notification_title">SimpleX Chat service</string>
|
||||
<string name="simplex_service_notification_text">Waiting for incoming messages</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user