From 0b00c2ad7612d30fcd96d24cafff7facecb51363 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 28 Feb 2022 10:44:48 +0000 Subject: [PATCH] android: receiving messages in background; ios: background task completion (#382) * android: receiving messages in background; ios: background task completion * complete receiving and sending messages in background --- apps/android/app/build.gradle | 1 + .../java/chat/simplex/app/MainActivity.kt | 6 +- .../main/java/chat/simplex/app/SimplexApp.kt | 43 +++++++-- .../java/chat/simplex/app/model/BGManager.kt | 3 +- .../java/chat/simplex/app/model/ChatModel.kt | 3 +- .../java/chat/simplex/app/model/NtfManager.kt | 7 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 47 +++++----- .../chat/simplex/app/views/chat/ChatView.kt | 25 +++-- .../simplex/app/views/helpers/AlertManager.kt | 3 +- .../simplex/app/views/helpers/ModalView.kt | 3 +- .../app/views/newchat/QRCodeScanner.kt | 7 +- .../views/usersettings/MarkdownHelpView.kt | 12 +-- .../app/views/usersettings/SettingsView.kt | 27 ++---- apps/ios/Shared/Model/ChatModel.swift | 3 + apps/ios/Shared/Model/NtfManager.swift | 1 + apps/ios/Shared/Model/SimpleXAPI.swift | 92 +++++++++++++++++-- .../Views/UserSettings/MarkdownHelp.swift | 12 +-- 17 files changed, 199 insertions(+), 96 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index ee66cd7c32..f129fd1784 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -71,6 +71,7 @@ dependencies { implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' + implementation 'androidx.lifecycle:lifecycle-process:2.4.1' implementation 'androidx.activity:activity-compose:1.4.0' implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' 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 a41051877d..20fff5ec83 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 @@ -76,7 +76,7 @@ fun processIntent(intent: Intent?, chatModel: ChatModel) { when (intent?.action) { NtfManager.OpenChatAction -> { val chatId = intent.getStringExtra("chatId") - Log.d("SIMPLEX", "processIntent: OpenChatAction $chatId") + Log.d(TAG, "processIntent: OpenChatAction $chatId") if (chatId != null) { val cInfo = chatModel.getChat(chatId)?.chatInfo if (cInfo != null) withApi { openChat(chatModel, cInfo) } @@ -90,7 +90,7 @@ fun processIntent(intent: Intent?, chatModel: ChatModel) { } fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) { - Log.d("SIMPLEX", "connectIfOpenedViaUri: opened via link") + Log.d(TAG, "connectIfOpenedViaUri: opened via link") if (chatModel.currentUser.value == null) { // TODO open from chat list view chatModel.appOpenUrl.value = uri @@ -102,7 +102,7 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) { confirmText = "Connect", onConfirm = { withApi { - Log.d("SIMPLEX", "connectIfOpenedViaUri: connecting") + Log.d(TAG, "connectIfOpenedViaUri: connecting") connectViaUri(chatModel, action, uri) } } 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 cb4f058c4b..0afefb1700 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,10 @@ package chat.simplex.app import android.app.Application -import android.net.LocalServerSocket +import android.content.Context +import android.net.* import android.util.Log +import androidx.lifecycle.* import androidx.work.* import chat.simplex.app.model.* import chat.simplex.app.views.helpers.withApi @@ -13,6 +15,8 @@ import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import kotlin.concurrent.thread +const val TAG = "SIMPLEX" + // ghc's rts external fun initHS() // android-support @@ -24,7 +28,7 @@ external fun chatInit(path: String): ChatCtrl external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String external fun chatRecvMsg(ctrl: ChatCtrl) : String -class SimplexApp: Application() { +class SimplexApp: Application(), LifecycleEventObserver { private lateinit var controller: ChatController lateinit var chatModel: ChatModel private lateinit var ntfManager: NtfManager @@ -43,6 +47,8 @@ class SimplexApp: Application() { override fun onCreate() { super.onCreate() + ProcessLifecycleOwner.get().lifecycle.addObserver(this) + registerNetworkCallback() ntfManager = NtfManager(applicationContext) val ctrl = chatInit(applicationContext.filesDir.toString()) controller = ChatController(ctrl, ntfManager, applicationContext) @@ -53,18 +59,43 @@ class SimplexApp: Application() { } } + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + Log.d(TAG, "onStateChanged: $event") + } + + private fun registerNetworkCallback() { + val connectivityManager = getSystemService(ConnectivityManager::class.java) + connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Log.e(TAG, "The default network is now: " + network) + } + + override fun onLost(network: Network) { + Log.e(TAG, "The application no longer has a default network. The last default network was " + network) + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + Log.e(TAG, "The default network changed capabilities: " + networkCapabilities) + } + + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { + Log.e(TAG, "The default network changed link properties: " + linkProperties) + } + }) + } + companion object { init { val socketName = "local.socket.address.listen.native.cmd2" val s = Semaphore(0) thread(name="stdout/stderr pipe") { - Log.d("SIMPLEX", "starting server") + Log.d(TAG, "starting server") val server = LocalServerSocket(socketName) - Log.d("SIMPLEX", "started server") + Log.d(TAG, "started server") s.release() val receiver = server.accept() - Log.d("SIMPLEX", "started receiver") + Log.d(TAG, "started receiver") val logbuffer = FifoQueue(500) if (receiver != null) { val inStream = receiver.inputStream @@ -73,7 +104,7 @@ class SimplexApp: Application() { while(true) { val line = input.readLine() ?: break - Log.d("SIMPLEX (stdout/stderr)", line) + Log.w("$TAG (stdout/stderr)", line) logbuffer.add(line) } } 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 index a511bf06fe..3089a5ed0c 100644 --- 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 @@ -3,6 +3,7 @@ package chat.simplex.app.model import android.content.Context import android.util.Log import androidx.work.* +import chat.simplex.app.TAG import chat.simplex.app.chatRecvMsg import java.util.concurrent.TimeUnit @@ -24,7 +25,7 @@ class BackgroundAPIWorker(appContext: Context, workerParams: WorkerParameters, c private fun getNewItems() { val json = chatRecvMsg(controller) val r = APIResponse.decodeStr(json).resp - Log.d("SIMPLEX", "chatRecvMsg: ${r.responseType}") + Log.d(TAG, "chatRecvMsg: ${r.responseType}") } private fun buildRequest(): OneTimeWorkRequest { 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 75b8c74220..96b08e86db 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 @@ -8,6 +8,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.style.TextDecoration import chat.simplex.app.ui.theme.SecretColor +import chat.simplex.app.ui.theme.SimplexBlue import kotlinx.datetime.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -650,7 +651,7 @@ enum class FormatColor(val color: String) { val uiColor: Color @Composable get() = when (this) { red -> Color.Red green -> Color.Green - blue -> Color.Blue + blue -> SimplexBlue yellow -> Color.Yellow cyan -> Color.Cyan magenta -> Color.Magenta diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt index 114cf22bf6..f5b54fd8eb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -6,8 +6,7 @@ import android.content.Intent import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import chat.simplex.app.MainActivity -import chat.simplex.app.R +import chat.simplex.app.* import kotlinx.datetime.Clock class NtfManager(val context: Context) { @@ -30,7 +29,7 @@ class NtfManager(val context: Context) { } fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) { - Log.d("SIMPLEX", "notifyMessageReceived ${cInfo.id}") + Log.d(TAG, "notifyMessageReceived ${cInfo.id}") val now = Clock.System.now().toEpochMilliseconds() val recentNotification = (now - prevNtfTime.getOrDefault(cInfo.id, 0) < msgNtfTimeoutMs) prevNtfTime[cInfo.id] = now @@ -64,7 +63,7 @@ class NtfManager(val context: Context) { } private fun getMsgPendingIntent(cInfo: ChatInfo) : PendingIntent{ - Log.d("SIMPLEX", "getMsgPendingIntent ${cInfo.id}") + Log.d(TAG, "getMsgPendingIntent ${cInfo.id}") val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt() val intent = Intent(context, MainActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) 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 9e4b899f79..aac062974b 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 @@ -5,8 +5,7 @@ import android.app.ActivityManager.RunningAppProcessInfo import android.content.Context import android.util.Log import androidx.compose.runtime.mutableStateOf -import chat.simplex.app.chatRecvMsg -import chat.simplex.app.chatSendCmd +import chat.simplex.app.* import chat.simplex.app.views.helpers.AlertManager import chat.simplex.app.views.helpers.withApi import kotlinx.coroutines.Dispatchers @@ -24,7 +23,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap var chatModel = ChatModel(this) suspend fun startChat(u: User) { - Log.d("SIMPLEX (user)", u.toString()) + Log.d(TAG, "user: $u") try { apiStartChat() chatModel.userAddress.value = apiGetUserAddress() @@ -32,9 +31,9 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap chatModel.currentUser = mutableStateOf(u) chatModel.userCreated.value = true startReceiver() - Log.d("SIMPLEX", "started chat") + Log.d(TAG, "started chat") } catch(e: Error) { - Log.d("SIMPLEX", "failed starting chat $e") + Log.e(TAG, "failed starting chat $e") throw e } } @@ -62,11 +61,11 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap val c = cmd.cmdString chatModel.terminalItems.add(TerminalItem.cmd(cmd)) val json = chatSendCmd(ctrl, c) - Log.d("SIMPLEX", "sendCmd: ${cmd.cmdType}") + Log.d(TAG, "sendCmd: ${cmd.cmdType}") val r = APIResponse.decodeStr(json) - Log.d("SIMPLEX", "sendCmd response type ${r.resp.responseType}") + Log.d(TAG, "sendCmd response type ${r.resp.responseType}") if (r.resp is CR.Response || r.resp is CR.Invalid) { - Log.d("SIMPLEX", "sendCmd response json $json") + Log.d(TAG, "sendCmd response json $json") } chatModel.terminalItems.add(TerminalItem.resp(r.resp)) r.resp @@ -77,8 +76,8 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap return withContext(Dispatchers.IO) { val json = chatRecvMsg(ctrl) val r = APIResponse.decodeStr(json).resp - Log.d("SIMPLEX", "chatRecvMsg: ${r.responseType}") - if (r is CR.Response || r is CR.Invalid) Log.d("SIMPLEX", "chatRecvMsg json: $json") + Log.d(TAG, "chatRecvMsg: ${r.responseType}") + if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json") r } } @@ -91,7 +90,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap suspend fun apiGetActiveUser(): User? { val r = sendCmd(CC.ShowActiveUser()) if (r is CR.ActiveUser) return r.user - Log.d("SIMPLEX", "apiGetActiveUser: ${r.responseType} ${r.details}") + Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}") chatModel.userCreated.value = false return null } @@ -99,7 +98,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap suspend fun apiCreateActiveUser(p: Profile): User { val r = sendCmd(CC.CreateActiveUser(p)) if (r is CR.ActiveUser) return r.user - Log.d("SIMPLEX", "apiCreateActiveUser: ${r.responseType} ${r.details}") + Log.d(TAG, "apiCreateActiveUser: ${r.responseType} ${r.details}") throw Error("user not created ${r.responseType} ${r.details}") } @@ -118,21 +117,21 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap suspend fun apiGetChat(type: ChatType, id: Long): Chat? { val r = sendCmd(CC.ApiGetChat(type, id)) if (r is CR.ApiChat ) return r.chat - Log.d("SIMPLEX", "apiGetChat bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}") return null } suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? { val r = sendCmd(CC.ApiSendMessage(type, id, mc)) if (r is CR.NewChatItem ) return r.chatItem - Log.d("SIMPLEX", "apiSendMessage bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiSendMessage bad response: ${r.responseType} ${r.details}") return null } suspend fun apiAddContact(): String? { val r = sendCmd(CC.AddContact()) if (r is CR.Invitation) return r.connReqInvitation - Log.d("SIMPLEX", "apiAddContact bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiAddContact bad response: ${r.responseType} ${r.details}") return null } @@ -182,21 +181,21 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap val r = sendCmd(CC.UpdateProfile(profile)) if (r is CR.UserProfileNoChange) return profile if (r is CR.UserProfileUpdated) return r.toProfile - Log.d("SIMPLEX", "apiUpdateProfile bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}") return null } suspend fun apiCreateUserAddress(): String? { val r = sendCmd(CC.CreateMyAddress()) if (r is CR.UserContactLinkCreated) return r.connReqContact - Log.d("SIMPLEX", "apiCreateUserAddress bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiCreateUserAddress bad response: ${r.responseType} ${r.details}") return null } suspend fun apiDeleteUserAddress(): Boolean { val r = sendCmd(CC.DeleteMyAddress()) if (r is CR.UserContactLinkDeleted) return true - Log.d("SIMPLEX", "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}") return false } @@ -207,34 +206,34 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap && r.chatError.storeError is StoreError.UserContactLinkNotFound) { return null } - Log.d("SIMPLEX", "apiGetUserAddress bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiGetUserAddress bad response: ${r.responseType} ${r.details}") return null } suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? { val r = sendCmd(CC.ApiAcceptContact(contactReqId)) if (r is CR.AcceptingContactRequest) return r.contact - Log.d("SIMPLEX", "apiAcceptContactRequest bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiAcceptContactRequest bad response: ${r.responseType} ${r.details}") return null } suspend fun apiRejectContactRequest(contactReqId: Long): Boolean { val r = sendCmd(CC.ApiRejectContact(contactReqId)) if (r is CR.ContactRequestRejected) return true - Log.d("SIMPLEX", "apiRejectContactRequest bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiRejectContactRequest bad response: ${r.responseType} ${r.details}") return false } suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean { val r = sendCmd(CC.ApiChatRead(type, id, range)) if (r is CR.CmdOk) return true - Log.d("SIMPLEX", "apiChatRead bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}") return false } fun apiErrorAlert(method: String, title: String, r: CR) { val errMsg = "${r.responseType}: ${r.details}" - Log.e("SIMPLEX", "$method bad response: $errMsg") + Log.e(TAG, "$method bad response: $errMsg") AlertManager.shared.showAlertMsg(title, errMsg) } @@ -286,7 +285,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap // NtfManager.shared.notifyMessageReceived(cInfo, cItem) // } else -> - Log.d("SIMPLEX" , "unsupported event: ${r.responseType}") + Log.d(TAG , "unsupported event: ${r.responseType}") } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 774afd42ef..d509cd97b3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import chat.simplex.app.TAG import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ChatItemView @@ -39,7 +40,7 @@ fun ChatView(chatModel: ChatModel) { BackHandler { chatModel.chatId.value = null } // TODO a more advanced version would mark as read only if in view LaunchedEffect(chat.chatItems) { - Log.d("SIMPLEX", "ChatView ${chatModel.chatId.value}: LaunchedEffect") + Log.d(TAG, "ChatView ${chatModel.chatId.value}: LaunchedEffect") delay(1000L) if (chat.chatItems.count() > 0) { chatModel.markChatItemsRead(chat.chatInfo) @@ -79,18 +80,16 @@ fun ChatLayout( info: () -> Unit, sendMessage: (String) -> Unit ) { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Scaffold( - topBar = { ChatInfoToolbar(chat, back, info) }, - bottomBar = { SendMsgView(sendMessage) }, - modifier = Modifier.navigationBarsWithImePadding() - ) { contentPadding -> - Surface( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colors.background) - ) { - ChatItemsList(chatItems) + Surface(Modifier.fillMaxWidth().background(MaterialTheme.colors.background)) { + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Scaffold( + topBar = { ChatInfoToolbar(chat, back, info) }, + bottomBar = { SendMsgView(sendMessage) }, + modifier = Modifier.navigationBarsWithImePadding() + ) { contentPadding -> + Box(Modifier.padding(contentPadding)) { + ChatItemsList(chatItems) + } } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt index 610ffc949b..f431e68e67 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt @@ -4,13 +4,14 @@ import android.util.Log import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf +import chat.simplex.app.TAG class AlertManager { var alertView = mutableStateOf<(@Composable () -> Unit)?>(null) var presentAlert = mutableStateOf(false) fun showAlert(alert: @Composable () -> Unit) { - Log.d("SIMPLEX", "AlertManager.showAlert") + Log.d(TAG, "AlertManager.showAlert") alertView.value = alert presentAlert.value = true } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt index e0401c7099..bb10bd639f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import chat.simplex.app.TAG import chat.simplex.app.views.helpers.CloseSheetBar @Composable @@ -36,7 +37,7 @@ class ModalManager { } fun showCustomModal(modal: @Composable (close: () -> Unit) -> Unit) { - Log.d("SIMPLEX", "ModalManager.showModal") + Log.d(TAG, "ModalManager.showModal") modalViews.add(modal) modalCount.value = modalViews.count() } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt index 8f2b940258..f6bee70490 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat +import chat.simplex.app.TAG import com.google.common.util.concurrent.ListenableFuture import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScanning @@ -62,7 +63,7 @@ fun QRCodeScanner(onBarcode: (String) -> Unit) { cameraProvider.unbindAll() cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) } catch (e: Exception) { - Log.d("SIMPLEX", "CameraPreview: ${e.localizedMessage}") + Log.d(TAG, "CameraPreview: ${e.localizedMessage}") } }, ContextCompat.getMainExecutor(context)) } @@ -90,11 +91,11 @@ class BarCodeAnalyser( if (barcodes.isNotEmpty()) { onBarcodeDetected(barcodes) } else { - Log.d("SIMPLEX", "BarcodeAnalyser: No barcode Scanned") + Log.d(TAG, "BarcodeAnalyser: No barcode Scanned") } } .addOnFailureListener { exception -> - Log.d("SIMPLEX", "BarcodeAnalyser: Something went wrong $exception") + Log.e(TAG, "BarcodeAnalyser: Something went wrong $exception") } .addOnCompleteListener { image.close() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt index 40def46dba..0f0a649f1e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt @@ -25,14 +25,14 @@ fun MarkdownHelpView() { "You can use markdown to format messages:", Modifier.padding(vertical = 16.dp) ) - MdFormat("*bold*", "bold text", Format.Bold()) - MdFormat("_italic_", "italic text", Format.Italic()) - MdFormat("~strike~", "strikethrough text", Format.StrikeThrough()) - MdFormat("`code`", "a = b + c", Format.Snippet()) + MdFormat("*bold*", "bold", Format.Bold()) + MdFormat("_italic_", "italic", Format.Italic()) + MdFormat("~strike~", "strike", Format.StrikeThrough()) + MdFormat("`a + b`", "a + b", Format.Snippet()) Row { MdSyntax("!1 colored!") Text(buildAnnotatedString { - withStyle(Format.Colored(FormatColor.red).style) { append("red text") } + withStyle(Format.Colored(FormatColor.red).style) { append("colored") } append(" (") appendColor(this, "1", FormatColor.red, ", ") appendColor(this, "2", FormatColor.green, ", ") @@ -46,7 +46,7 @@ fun MarkdownHelpView() { MdSyntax("#secret#") SelectionContainer { Text(buildAnnotatedString { - withStyle(Format.Secret().style) { append("secret text") } + withStyle(Format.Secret().style) { append("secret") } }) } } 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 0e55619137..b9b4e69cc5 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 @@ -30,10 +30,7 @@ fun SettingsView(chatModel: ChatModel) { if (user != null) { SettingsLayout( profile = user.profile, -// showModal = { modal -> ModalManager.shared.showModal { modal(chatModel) } }, - showProfile = { ModalManager.shared.showModal { UserProfileView(chatModel) } }, - showAddress = { ModalManager.shared.showModal { UserAddressView(chatModel) } }, - showHelp = { ModalManager.shared.showModal { HelpView(chatModel) } }, + showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } }, showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } } ) } @@ -45,10 +42,7 @@ val simplexTeamUri = @Composable fun SettingsLayout( profile: Profile, -// showModal: (modal: @Composable (ChatModel) -> Unit) -> Unit, - showProfile: () -> Unit, - showAddress: () -> Unit, - showHelp: () -> Unit, + showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showTerminal: () -> Unit ) { val uriHandler = LocalUriHandler.current @@ -67,10 +61,11 @@ fun SettingsLayout( Text( "Your Settings", style = MaterialTheme.typography.h1, + modifier = Modifier.padding(start = 8.dp) ) Spacer(Modifier.height(30.dp)) - SettingsSectionView(showProfile, 60.dp) { + SettingsSectionView(showModal { UserProfileView(it) }, 60.dp) { Icon( Icons.Outlined.AccountCircle, contentDescription = "Avatar Placeholder", @@ -86,7 +81,7 @@ fun SettingsLayout( } } Divider(Modifier.padding(horizontal = 8.dp)) - SettingsSectionView(showAddress) { + SettingsSectionView(showModal { UserAddressView(it) }) { Icon( Icons.Outlined.QrCode, contentDescription = "Address", @@ -96,7 +91,7 @@ fun SettingsLayout( } Spacer(Modifier.height(24.dp)) - SettingsSectionView(showHelp) { + SettingsSectionView(showModal { HelpView(it) }) { Icon( Icons.Outlined.HelpOutline, contentDescription = "Chat help", @@ -104,7 +99,7 @@ fun SettingsLayout( Spacer(Modifier.padding(horizontal = 4.dp)) Text("How to use SimpleX Chat") } - SettingsSectionView({ ModalManager.shared.showModal { MarkdownHelpView() } }) { + SettingsSectionView(showModal { MarkdownHelpView() }) { Icon( Icons.Outlined.TextFormat, contentDescription = "Markdown help", @@ -167,12 +162,12 @@ fun SettingsLayout( } @Composable -fun SettingsSectionView(func: () -> Unit, height: Dp = 48.dp, content: (@Composable () -> Unit)) { +fun SettingsSectionView(click: () -> Unit, height: Dp = 48.dp, content: (@Composable () -> Unit)) { Row( Modifier .padding(start = 8.dp) .fillMaxWidth() - .clickable(onClick = func) + .clickable(onClick = click) .height(height), verticalAlignment = Alignment.CenterVertically ) { @@ -191,9 +186,7 @@ fun PreviewSettingsLayout() { SimpleXTheme { SettingsLayout( profile = Profile.sampleData, - showProfile = {}, - showAddress = {}, - showHelp = {}, + showModal = {{}}, showTerminal = {} ) } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index c8d37b3b65..d3d80e070c 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -22,6 +22,9 @@ final class ChatModel: ObservableObject { @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: String? @Published var appOpenUrl: URL? + + var messageDelivery: Dictionary Void> = [:] + static let shared = ChatModel() func hasChat(_ id: String) -> Bool { diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 5459d9b673..3f0bbca448 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -178,6 +178,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { if let s = body { content.body = s } content.targetContentIdentifier = targetContentIdentifier content.userInfo = userInfo + // TODO move logic of adding sound here, so it applies to background notifications too content.sound = .default // content.interruptionLevel = .active // content.relevanceScore = 0.5 // 0-1 diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 16b38c19b9..a6a45a8f1c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -241,10 +241,53 @@ enum TerminalItem: Identifiable { } } -func chatSendCmdSync(_ cmd: ChatCommand) -> ChatResponse { +private func _sendCmd(_ cmd: ChatCommand) -> ChatResponse { var c = cmd.cmdString.cString(using: .utf8)! + return chatResponse(chat_send_cmd(getChatCtrl(), &c)) +} + +private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) { + var id: UIBackgroundTaskIdentifier! + var running = true + let endTask = { +// logger.debug("beginBGTask: endTask \(id.rawValue)") + if running { + running = false + if let h = handler { +// logger.debug("beginBGTask: user handler") + h() + } + if id != .invalid { + UIApplication.shared.endBackgroundTask(id) + id = .invalid + } + } + } + id = UIApplication.shared.beginBackgroundTask(expirationHandler: endTask) +// logger.debug("beginBGTask: \(id.rawValue)") + return endTask +} + +let msgDelay: Double = 7.5 +let maxTaskDuration: Double = 15 + +private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse) -> ChatResponse { + let endTask = beginBGTask() + DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask) + let r = f() + if let d = bgDelay { + DispatchQueue.global().asyncAfter(deadline: .now() + d, execute: endTask) + } else { + endTask() + } + return r +} + +func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) -> ChatResponse { logger.debug("chatSendCmd \(cmd.cmdType)") - let resp = chatResponse(chat_send_cmd(getChatCtrl(), &c)) + let resp = bgTask + ? withBGTask(bgDelay: bgDelay) { _sendCmd(cmd) } + : _sendCmd(cmd) logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") if case let .response(_, json) = resp { logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") @@ -256,16 +299,19 @@ func chatSendCmdSync(_ cmd: ChatCommand) -> ChatResponse { return resp } -func chatSendCmd(_ cmd: ChatCommand) async -> ChatResponse { +func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse { await withCheckedContinuation { cont in - cont.resume(returning: chatSendCmdSync(cmd)) + cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay)) } } func chatRecvMsg() async -> ChatResponse { await withCheckedContinuation { cont in - let resp = chatResponse(chat_recv_msg(getChatCtrl())!) - cont.resume(returning: resp) + _ = withBGTask(bgDelay: msgDelay) { + let resp = chatResponse(chat_recv_msg(getChatCtrl())!) + cont.resume(returning: resp) + return resp + } } } @@ -304,13 +350,30 @@ func apiGetChat(type: ChatType, id: Int64) async throws -> Chat { } func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) async throws -> ChatItem { - let r = await chatSendCmd(.apiSendMessage(type: type, id: id, msg: msg)) - if case let .newChatItem(aChatItem) = r { return aChatItem.chatItem } + let chatModel = ChatModel.shared + let cmd = ChatCommand.apiSendMessage(type: type, id: id, msg: msg) + let r: ChatResponse + if type == .direct { + var cItem: ChatItem! + let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } }) + r = await chatSendCmd(cmd, bgTask: false) + if case let .newChatItem(aChatItem) = r { + cItem = aChatItem.chatItem + chatModel.messageDelivery[cItem.id] = endTask + return cItem + } + endTask() + } else { + r = await chatSendCmd(cmd, bgDelay: msgDelay) + if case let .newChatItem(aChatItem) = r { + return aChatItem.chatItem + } + } throw r } func apiAddContact() throws -> String { - let r = chatSendCmdSync(.addContact) + let r = chatSendCmdSync(.addContact, bgTask: false) if case let .invitation(connReqInvitation) = r { return connReqInvitation } throw r } @@ -325,7 +388,7 @@ func apiConnect(connReq: String) async throws { } func apiDeleteChat(type: ChatType, id: Int64) async throws { - let r = await chatSendCmd(.apiDeleteChat(type: type, id: id)) + let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false) if case .contactDeleted = r { return } throw r } @@ -450,6 +513,8 @@ class ChatReceiver { self._lastMsgTime = .now processReceivedMsg(msg) if self.receiveMessages { + do { try await Task.sleep(nanoseconds: 7_500_000) } + catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") } await receiveMsgLoop() } } @@ -508,6 +573,13 @@ func processReceivedMsg(_ res: ChatResponse) { let cItem = aChatItem.chatItem if chatModel.upsertChatItem(cInfo, cItem) { NtfManager.shared.notifyMessageReceived(cInfo, cItem) + } else if let endTask = chatModel.messageDelivery[cItem.id] { + switch cItem.meta.itemStatus { + case .sndSent: endTask() + case .sndErrorAuth: endTask() + case .sndError: endTask() + default: break + } } default: logger.debug("unsupported event: \(res.responseType)") diff --git a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift index d9a73372dd..855d0adff1 100644 --- a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift @@ -13,13 +13,13 @@ struct MarkdownHelp: View { VStack(alignment: .leading, spacing: 8) { Text("You can use markdown to format messages:") .padding(.bottom) - mdFormat("*bold*", Text("bold text").bold()) - mdFormat("_italic_", Text("italic text").italic()) - mdFormat("~strike~", Text("strikethrough text").strikethrough()) - mdFormat("`code`", Text("`a = b + c`").font(.body.monospaced())) - mdFormat("!1 colored!", Text("red text").foregroundColor(.red) + Text(" (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(")")) + mdFormat("*bold*", Text("bold").bold()) + mdFormat("_italic_", Text("italic").italic()) + mdFormat("~strike~", Text("strike").strikethrough()) + mdFormat("`a + b`", Text("`a + b`").font(.body.monospaced())) + mdFormat("!1 colored!", Text("colored").foregroundColor(.red) + Text(" (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(")")) ( - mdFormat("#secret#", Text("secret text") + mdFormat("#secret#", Text("secret") .foregroundColor(.clear) .underline(color: .primary) + Text(" (can be copied)")) )