diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index baef53847..9d5670a4e 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "chat.simplex.app" minSdk 29 targetSdk 32 - versionCode 37 + versionCode 38 versionName "3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/apps/android/app/src/main/cpp/simplex-api.c b/apps/android/app/src/main/cpp/simplex-api.c index e9178df5c..5be1d1fad 100644 --- a/apps/android/app/src/main/cpp/simplex-api.c +++ b/apps/android/app/src/main/cpp/simplex-api.c @@ -24,9 +24,11 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass // from simplex-chat typedef void* chat_ctrl; -extern chat_ctrl chat_init(const char * path); +extern chat_ctrl chat_init(const char *path); extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); extern char *chat_recv_msg(chat_ctrl ctrl); +extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); +extern char *chat_parse_markdown(const char *str); JNIEXPORT jlong JNICALL Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) { @@ -48,3 +50,16 @@ JNIEXPORT jstring JNICALL Java_chat_simplex_app_SimplexAppKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) { return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller)); } + +JNIEXPORT jstring JNICALL +Java_chat_simplex_app_SimplexAppKt_chatRecvMsgWait(JNIEnv *env, __unused jclass clazz, jlong controller, jint wait) { + return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait)); +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_app_SimplexAppKt_chatParseMarkdown(JNIEnv *env, __unused jclass clazz, jstring str) { + const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str)); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} 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 73ecd073d..69cae0dd2 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 @@ -24,8 +24,10 @@ external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long external fun chatInit(path: String): ChatCtrl -external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String -external fun chatRecvMsg(ctrl: ChatCtrl) : String +external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String +external fun chatRecvMsg(ctrl: ChatCtrl): String +external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String +external fun chatParseMarkdown(str: String): String class SimplexApp: Application(), LifecycleEventObserver { val chatController: ChatController by lazy { @@ -55,7 +57,6 @@ class SimplexApp: Application(), LifecycleEventObserver { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo } else { chatController.startChat(user) - SimplexService.start(applicationContext) chatController.showBackgroundServiceNoticeIfNeeded() } } @@ -66,9 +67,9 @@ class SimplexApp: Application(), LifecycleEventObserver { withApi { when (event) { Lifecycle.Event.ON_STOP -> - if (!appPreferences.runServiceInBackground.get()) SimplexService.stop(applicationContext) + if (appPreferences.runServiceInBackground.get()) SimplexService.start(applicationContext) Lifecycle.Event.ON_START -> - SimplexService.start(applicationContext) + SimplexService.stop(applicationContext) Lifecycle.Event.ON_RESUME -> if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { chatController.showBackgroundServiceNoticeIfNeeded() 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 index 94ae6a737..db62727d2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt @@ -20,6 +20,7 @@ class SimplexService: Service() { private var wakeLock: PowerManager.WakeLock? = null private var isServiceStarted = false private var isStartingService = false + private var isStoppingService = false private var notificationManager: NotificationManager? = null private var serviceNotification: Notification? = null private val chatController by lazy { (application as SimplexApp).chatController } @@ -71,7 +72,6 @@ class SimplexService: Service() { } else { 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 { @@ -88,6 +88,8 @@ class SimplexService: Service() { private fun stopService() { Log.d(TAG, "Stopping foreground service") + if (!isServiceStarted || isStoppingService) return + isStoppingService = true try { wakeLock?.let { while (it.isHeld) it.release() // release all, in case acquired more than once @@ -98,7 +100,7 @@ class SimplexService: Service() { } catch (e: Exception) { Log.d(TAG, "Service stopped without being started: ${e.message}") } - + isStoppingService = false isServiceStarted = false saveServiceState(this, ServiceState.STOPPED) } @@ -121,7 +123,7 @@ class SimplexService: Service() { PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) } return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.ntf_icon) + .setSmallIcon(R.drawable.ntf_service_icon) .setColor(0x88FFFF) .setContentTitle(title) .setContentText(text) 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 10e40f30d..598112224 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 @@ -117,8 +117,11 @@ class AppPreferences(val context: Context) { } } +private const val MESSAGE_TIMEOUT: Int = 15_000_000 + open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) { val chatModel = ChatModel(this) + private var receiverStarted = false init { chatModel.runServiceInBackground.value = appPrefs.runServiceInBackground.get() @@ -142,6 +145,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager chatModel.currentUser.value = user chatModel.userCreated.value = true chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete + startReceiver() Log.d(TAG, "chat started") } catch (e: Error) { Log.e(TAG, "failed starting chat $e") @@ -149,8 +153,9 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - fun startReceiver() { + private fun startReceiver() { Log.d(TAG, "ChatController startReceiver") + if (receiverStarted) return thread(name="receiver") { GlobalScope.launch { withContext(Dispatchers.IO) { recvMspLoop() } } } @@ -176,18 +181,23 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - suspend fun recvMsg(): CR { + private suspend fun recvMsg(): CR? { return withContext(Dispatchers.IO) { - val json = chatRecvMsg(ctrl) - val r = APIResponse.decodeStr(json).resp - Log.d(TAG, "chatRecvMsg: ${r.responseType}") - if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json") - r + val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) + if (json == "") { + null + } else { + val r = APIResponse.decodeStr(json).resp + Log.d(TAG, "chatRecvMsg: ${r.responseType}") + if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json") + r + } } } - suspend fun recvMspLoop() { - processReceivedMsg(recvMsg()) + private suspend fun recvMspLoop() { + val msg = recvMsg() + if (msg != null) processReceivedMsg(msg) recvMspLoop() } @@ -206,7 +216,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager throw Error("user not created ${r.responseType} ${r.details}") } - suspend fun apiStartChat(): Boolean { + private suspend fun apiStartChat(): Boolean { val r = sendCmd(CC.StartChat()) when (r) { is CR.ChatStarted -> return true @@ -215,13 +225,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - suspend fun apiSetFilesFolder(filesFolder: String) { + private suspend fun apiSetFilesFolder(filesFolder: String) { val r = sendCmd(CC.SetFilesFolder(filesFolder)) if (r is CR.CmdOk) return throw Error("failed to set files folder: ${r.responseType} ${r.details}") } - suspend fun apiGetChats(): List { + private suspend fun apiGetChats(): List { val r = sendCmd(CC.ApiGetChats()) if (r is CR.ApiChats ) return r.chats throw Error("failed getting the list of chats: ${r.responseType} ${r.details}") @@ -256,7 +266,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager return null } - suspend fun getUserSMPServers(): List? { + private suspend fun getUserSMPServers(): List? { val r = sendCmd(CC.GetUserSMPServers()) if (r is CR.UserSMPServers) return r.smpServers Log.e(TAG, "getUserSMPServers bad response: ${r.responseType} ${r.details}") @@ -383,7 +393,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager return false } - suspend fun apiGetUserAddress(): String? { + private suspend fun apiGetUserAddress(): String? { val r = sendCmd(CC.ShowMyAddress()) if (r is CR.UserContactLink) return r.connReqContact if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore 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 833977b98..862e47684 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 @@ -114,7 +114,6 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String) { Profile(displayName, fullName, null) ) chatModel.controller.startChat(user) - SimplexService.start(chatModel.controller.appContext) chatModel.controller.showBackgroundServiceNoticeIfNeeded() chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete } diff --git a/apps/android/app/src/main/res/drawable-hdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-hdpi/ntf_service_icon.png new file mode 100644 index 000000000..2bdab389b Binary files /dev/null and b/apps/android/app/src/main/res/drawable-hdpi/ntf_service_icon.png differ diff --git a/apps/android/app/src/main/res/drawable-mdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-mdpi/ntf_service_icon.png new file mode 100644 index 000000000..8d71e704d Binary files /dev/null and b/apps/android/app/src/main/res/drawable-mdpi/ntf_service_icon.png differ diff --git a/apps/android/app/src/main/res/drawable-xhdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-xhdpi/ntf_service_icon.png new file mode 100644 index 000000000..950a78316 Binary files /dev/null and b/apps/android/app/src/main/res/drawable-xhdpi/ntf_service_icon.png differ diff --git a/apps/android/app/src/main/res/drawable-xxhdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-xxhdpi/ntf_service_icon.png new file mode 100644 index 000000000..81da12bb9 Binary files /dev/null and b/apps/android/app/src/main/res/drawable-xxhdpi/ntf_service_icon.png differ diff --git a/apps/android/app/src/main/res/drawable-xxxhdpi/ntf_service_icon.png b/apps/android/app/src/main/res/drawable-xxxhdpi/ntf_service_icon.png new file mode 100644 index 000000000..4ba82576b Binary files /dev/null and b/apps/android/app/src/main/res/drawable-xxxhdpi/ntf_service_icon.png differ diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index dcee1bf91..0f93abd8b 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -179,6 +179,7 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r { return NtfMessages(connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages) } + logger.debug("apiGetNtfMessage ignored response: \(String.init(describing: r), privacy: .public)") return nil }