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 1e5cb7dd62..4646474f0a 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 @@ -72,7 +72,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { val m = vm.chatModel val lastLAVal = lastLA.value if ( - m.controller.prefPerformLA.get() + m.controller.appPrefs.performLA.get() && (lastLAVal == null || (System.nanoTime() - lastLAVal >= 30 * 1e+9)) ) { userAuthorized.value = false @@ -91,7 +91,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { LAResult.Unavailable -> { userAuthorized.value = true m.performLA.value = false - m.controller.prefPerformLA.set(false) + m.controller.appPrefs.performLA.set(false) laUnavailableTurningOffAlert() } } @@ -106,13 +106,13 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { } private fun schedulePeriodicServiceRestartWorker() { - val workerVersion = chatController.prefAutoRestartWorkerVersion.get() + val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get() 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.prefAutoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION) + chatController.appPrefs.autoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION) ExistingPeriodicWorkPolicy.REPLACE } val work = PeriodicWorkRequestBuilder(SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES) @@ -126,33 +126,34 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { private fun setPerformLA(on: Boolean) { val m = vm.chatModel if (on) { - m.controller.prefLANoticeShown.set(true) + m.controller.appPrefs.laNoticeShown.set(true) authenticate( generalGetString(R.string.auth_enable), generalGetString(R.string.auth_confirm_credential), this@MainActivity, completed = { laResult -> + val prefPerformLA = m.controller.appPrefs.performLA when (laResult) { LAResult.Success -> { m.performLA.value = true - m.controller.prefPerformLA.set(true) + prefPerformLA.set(true) userAuthorized.value = true lastLA.value = System.nanoTime() laTurnedOnAlert() } is LAResult.Error -> { m.performLA.value = false - m.controller.prefPerformLA.set(false) + prefPerformLA.set(false) laErrorToast(applicationContext, laResult.errString) } LAResult.Failed -> { m.performLA.value = false - m.controller.prefPerformLA.set(false) + prefPerformLA.set(false) laFailedToast(applicationContext) } LAResult.Unavailable -> { m.performLA.value = false - m.controller.prefPerformLA.set(false) + prefPerformLA.set(false) laUnavailableInstructionAlert() } } @@ -164,24 +165,25 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { generalGetString(R.string.auth_confirm_credential), this@MainActivity, completed = { laResult -> + val prefPerformLA = m.controller.appPrefs.performLA when (laResult) { LAResult.Success -> { m.performLA.value = false - m.controller.prefPerformLA.set(false) + prefPerformLA.set(false) } is LAResult.Error -> { m.performLA.value = true - m.controller.prefPerformLA.set(true) + prefPerformLA.set(true) laErrorToast(applicationContext, laResult.errString) } LAResult.Failed -> { m.performLA.value = true - m.controller.prefPerformLA.set(true) + prefPerformLA.set(true) laFailedToast(applicationContext) } LAResult.Unavailable -> { m.performLA.value = false - m.controller.prefPerformLA.set(false) + prefPerformLA.set(false) laUnavailableTurningOffAlert() } } @@ -212,7 +214,7 @@ fun MainPage( var showAdvertiseLAAlert by remember { mutableStateOf(false) } LaunchedEffect(showAdvertiseLAAlert) { if ( - !chatModel.controller.prefLANoticeShown.get() + !chatModel.controller.appPrefs.laNoticeShown.get() && showAdvertiseLAAlert && chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && chatModel.chats.isNotEmpty() @@ -226,6 +228,12 @@ fun MainPage( laUnavailableInstructionAlert() } } + LaunchedEffect(chatModel.clearOverlays.value) { + if (chatModel.clearOverlays.value) { + ModalManager.shared.closeModals() + chatModel.clearOverlays.value = false + } + } Box { val onboarding = chatModel.onboardingStage.value val userCreated = chatModel.userCreated.value @@ -240,8 +248,6 @@ fun MainPage( if (chatModel.chatId.value == null) ChatListView(chatModel, setPerformLA = { setPerformLA(it) }) else ChatView(chatModel) } - val invitation = chatModel.activeCallInvitation.value - if (invitation != null) IncomingCallAlertView(invitation, chatModel) } } onboarding == OnboardingStage.Step1_SimpleXInfo -> @@ -251,6 +257,8 @@ fun MainPage( onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) } ModalManager.shared.showInView() + val invitation = chatModel.activeCallInvitation.value + if (invitation != null) IncomingCallAlertView(invitation, chatModel) AlertManager.shared.showInView() } } @@ -273,7 +281,9 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) { } NtfManager.AcceptCallAction -> { val chatId = intent.getStringExtra("chatId") + if (chatId == null || chatId == "") return Log.d(TAG, "processNotificationIntent: AcceptCallAction $chatId") + chatModel.clearOverlays.value = true val invitation = chatModel.callInvitations[chatId] if (invitation == null) { AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended)) 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 1a7988fdd7..a200f99cc3 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 @@ -29,7 +29,7 @@ external fun chatRecvMsg(ctrl: ChatCtrl) : String class SimplexApp: Application(), LifecycleEventObserver { val chatController: ChatController by lazy { val ctrl = chatInit(getFilesDirectory(applicationContext)) - ChatController(ctrl, ntfManager, applicationContext) + ChatController(ctrl, ntfManager, applicationContext, appPreferences) } val chatModel: ChatModel by lazy { @@ -37,7 +37,11 @@ class SimplexApp: Application(), LifecycleEventObserver { } private val ntfManager: NtfManager by lazy { - NtfManager(applicationContext) + NtfManager(applicationContext, appPreferences) + } + + private val appPreferences: AppPreferences by lazy { + AppPreferences(applicationContext) } override fun onCreate() { @@ -61,7 +65,7 @@ class SimplexApp: Application(), LifecycleEventObserver { withApi { when (event) { Lifecycle.Event.ON_STOP -> - if (!chatController.prefRunServiceInBackground.get()) SimplexService.stop(applicationContext) + if (!appPreferences.runServiceInBackground.get()) SimplexService.stop(applicationContext) Lifecycle.Event.ON_START -> SimplexService.start(applicationContext) Lifecycle.Event.ON_RESUME -> 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 0ba0004d71..404839d9db 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 @@ -1,6 +1,7 @@ package chat.simplex.app.model import android.app.* +import android.app.Notification.VISIBILITY_PUBLIC import android.content.* import android.graphics.BitmapFactory import android.media.AudioAttributes @@ -14,7 +15,7 @@ import chat.simplex.app.views.helpers.base64ToBitmap import chat.simplex.app.views.helpers.generalGetString import kotlinx.datetime.Clock -class NtfManager(val context: Context) { +class NtfManager(val context: Context, private val appPreferences: AppPreferences) { companion object { const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION" const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION" @@ -64,21 +65,25 @@ class NtfManager(val context: Context) { } fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) { - Log.d(TAG, "notifyMessageReceived ${cInfo.id}") + notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + } + + fun notifyMessageReceived(chatId: String, displayName: String, msgText: String) { + Log.d(TAG, "notifyMessageReceived $chatId") val now = Clock.System.now().toEpochMilliseconds() - val recentNotification = (now - prevNtfTime.getOrDefault(cInfo.id, 0) < msgNtfTimeoutMs) - prevNtfTime[cInfo.id] = now + val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs) + prevNtfTime[chatId] = now val notification = NotificationCompat.Builder(context, MessageChannel) - .setContentTitle(cInfo.displayName) - .setContentText(hideSecrets(cItem)) + .setContentTitle(displayName) + .setContentText(msgText) .setPriority(NotificationCompat.PRIORITY_HIGH) .setGroup(MessageGroup) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setSmallIcon(R.drawable.ntf_icon) .setColor(0x88FFFF) .setAutoCancel(true) - .setContentIntent(chatPendingIntent(OpenChatAction, cInfo.id)) + .setContentIntent(chatPendingIntent(OpenChatAction, chatId)) .setSilent(recentNotification) .build() @@ -93,7 +98,7 @@ class NtfManager(val context: Context) { with(NotificationManagerCompat.from(context)) { // using cInfo.id only shows one notification per chat and updates it when the message arrives - notify(cInfo.id.hashCode(), notification) + notify(chatId.hashCode(), notification) notify(0, summary) } } @@ -105,11 +110,12 @@ class NtfManager(val context: Context) { val keyguardManager = getKeyguardManager(context) val image = invitation.contact.image var ntfBuilder = - if (keyguardManager.isDeviceLocked) { + if (keyguardManager.isDeviceLocked && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) { val fullScreenIntent = Intent(context, IncomingCallActivity::class.java) - val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) NotificationCompat.Builder(context, LockScreenCallChannel) .setFullScreenIntent(fullScreenPendingIntent, true) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSilent(true) } else { val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once) 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 c834779461..3f9d86b532 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 @@ -46,22 +46,76 @@ fun isAppOnForeground(context: Context): Boolean { return false } -open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context) { - var chatModel = ChatModel(this) - private val sharedPreferences: SharedPreferences = appContext.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) +enum class CallOnLockScreen { + DISABLE, + SHOW, + ACCEPT; - val prefRunServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true) - val prefBackgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false) - private val prefBackgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false) - val prefAutoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0) - val prefWebrtcPolicyRelay = mkBoolPreference(SHARED_PREFS_WEBRTC_POLICY_RELAY, true) - val prefAcceptCallsFromLockScreen = mkBoolPreference(SHARED_PREFS_WEBRTC_ACCEPT_CALLS_FROM_LOCK_SCREEN, false) - val prefPerformLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false) - val prefLANoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false) + companion object { + val default = SHOW + } +} + +class AppPreferences(val context: Context) { + private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) + + val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true) + val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false) + val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false) + val autoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0) + val webrtcPolicyRelay = mkBoolPreference(SHARED_PREFS_WEBRTC_POLICY_RELAY, true) + private val _callOnLockScreen = mkStrPreference(SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN, CallOnLockScreen.default.name) + val callOnLockScreen: Preference = Preference( + get = fun(): CallOnLockScreen { + val value = _callOnLockScreen.get() ?: return CallOnLockScreen.default + return try { + CallOnLockScreen.valueOf(value) + } catch (e: Error) { + CallOnLockScreen.default + } + }, + set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) } + ) + val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false) + val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false) + + private fun mkIntPreference(prefName: String, default: Int) = + Preference( + get = fun() = sharedPreferences.getInt(prefName, default), + set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply() + ) + + private fun mkBoolPreference(prefName: String, default: Boolean) = + Preference( + get = fun() = sharedPreferences.getBoolean(prefName, default), + set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).apply() + ) + + private fun mkStrPreference(prefName: String, default: String?): Preference = + Preference( + get = fun() = sharedPreferences.getString(prefName, default), + set = fun(value) = sharedPreferences.edit().putString(prefName, value).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" + private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown" + private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown" + private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay" + private const val SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN = "CallsOnLockScreen" + private const val SHARED_PREFS_PERFORM_LA = "PerformLA" + private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown" + } +} + +open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) { + val chatModel = ChatModel(this) init { - chatModel.runServiceInBackground.value = prefRunServiceInBackground.get() - chatModel.performLA.value = prefPerformLA.get() + chatModel.runServiceInBackground.value = appPrefs.runServiceInBackground.get() + chatModel.performLA.value = appPrefs.performLA.get() } suspend fun startChat(user: User) { @@ -511,7 +565,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } is CR.CallInvitation -> { - val invitation = CallInvitation(r.contact, r.callType.media, r.sharedKey) + val invitation = CallInvitation(r.contact, r.callType.media, r.sharedKey, r.callTs) chatModel.callManager.reportNewIncomingCall(invitation) } is CR.CallOffer -> { @@ -519,7 +573,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager // TODO check encryption is compatible withCall(r, r.contact) { call -> chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey) - chatModel.callCommand.value = WCallCommand.Offer(offer = r.offer.rtcSession, iceCandidates = r.offer.rtcIceCandidates, media = r.callType.media, aesKey = r.sharedKey) + val useRelay = chatModel.controller.appPrefs.webrtcPolicyRelay.get() + chatModel.callCommand.value = WCallCommand.Offer(offer = r.offer.rtcSession, iceCandidates = r.offer.rtcIceCandidates, media = r.callType.media, aesKey = r.sharedKey, relay = useRelay) } } is CR.CallAnswer -> { @@ -592,7 +647,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager fun showBackgroundServiceNoticeIfNeeded() { Log.d(TAG, "showBackgroundServiceNoticeIfNeeded") - if (!prefBackgroundServiceNoticeShown.get()) { + if (!appPrefs.backgroundServiceNoticeShown.get()) { // the branch for the new users who has never seen service notice if (isIgnoringBatteryOptimizations(appContext)) { showBGServiceNotice() @@ -600,20 +655,20 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager showBGServiceNoticeIgnoreOptimization() } // set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice - prefBackgroundServiceNoticeShown.set(true) - prefBackgroundServiceBatteryNoticeShown.set(true) - } else if (!isIgnoringBatteryOptimizations(appContext) && prefRunServiceInBackground.get()) { + appPrefs.backgroundServiceNoticeShown.set(true) + appPrefs.backgroundServiceBatteryNoticeShown.set(true) + } else if (!isIgnoringBatteryOptimizations(appContext) && appPrefs.runServiceInBackground.get()) { // the branch for users who have app installed, and have seen the service notice, // but the battery optimization for the app is on (Android 12) AND the service is running - if (prefBackgroundServiceBatteryNoticeShown.get()) { + if (appPrefs.backgroundServiceBatteryNoticeShown.get()) { // users have been presented with battery notice before - they did not allow ignoring optimizitions -> disable service showDisablingServiceNotice() - prefRunServiceInBackground.set(false) + appPrefs.runServiceInBackground.set(false) chatModel.runServiceInBackground.value = false } else { // show battery optimization notice showBGServiceNoticeIgnoreOptimization() - prefBackgroundServiceBatteryNoticeShown.set(true) + appPrefs.backgroundServiceBatteryNoticeShown.set(true) } } } @@ -704,8 +759,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager fun showLANotice(activity: FragmentActivity) { Log.d(TAG, "showLANotice") - if (!prefLANoticeShown.get()) { - prefLANoticeShown.set(true) + if (!appPrefs.laNoticeShown.get()) { + appPrefs.laNoticeShown.set(true) AlertManager.shared.showAlertDialog( title = generalGetString(R.string.la_notice_title), text = generalGetString(R.string.la_notice_text), @@ -719,22 +774,22 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager when (laResult) { LAResult.Success -> { chatModel.performLA.value = true - prefPerformLA.set(true) + appPrefs.performLA.set(true) laTurnedOnAlert() } is LAResult.Error -> { chatModel.performLA.value = false - prefPerformLA.set(false) + appPrefs.performLA.set(false) laErrorToast(appContext, laResult.errString) } LAResult.Failed -> { chatModel.performLA.value = false - prefPerformLA.set(false) + appPrefs.performLA.set(false) laFailedToast(appContext) } LAResult.Unavailable -> { chatModel.performLA.value = false - prefPerformLA.set(false) + appPrefs.performLA.set(false) chatModel.showAdvertiseLAUnavailableAlert.value = true } } @@ -763,30 +818,6 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager context.startActivity(this) } } - - private fun mkIntPreference(prefName: String, default: Int) = - Preference( - get = fun() = sharedPreferences.getInt(prefName, default), - set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply() - ) - - private fun mkBoolPreference(prefName: String, default: Boolean) = - Preference( - get = fun() = sharedPreferences.getBoolean(prefName, default), - set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).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" - private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown" - private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown" - private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay" - private const val SHARED_PREFS_WEBRTC_ACCEPT_CALLS_FROM_LOCK_SCREEN = "AcceptCallsFromLockScreen" - private const val SHARED_PREFS_PERFORM_LA = "PerformLA" - private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown" - } } class Preference(val get: () -> T, val set: (T) -> Unit) @@ -982,7 +1013,7 @@ sealed class CR { @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndGroupFileCancelled") class SndGroupFileCancelled(val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List): CR() - @Serializable @SerialName("callInvitation") class CallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null): CR() + @Serializable @SerialName("callInvitation") class CallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant): CR() @Serializable @SerialName("callOffer") class CallOffer(val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() @Serializable @SerialName("callAnswer") class CallAnswer(val contact: Contact, val answer: WebRTCSession): CR() @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() 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 6703df1aa0..833977b982 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 @@ -87,7 +87,7 @@ fun CreateProfilePanel(chatModel: ChatModel) { val createModifier: Modifier val createColor: Color if (enabled) { - createModifier = Modifier.padding(8.dp).clickable { createProfile(chatModel, displayName.value, fullName.value) } + createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value) }.padding(8.dp) createColor = MaterialTheme.colors.primary } else { createModifier = Modifier.padding(8.dp) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt index 13bad493b1..b0c8a442be 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt @@ -3,19 +3,28 @@ package chat.simplex.app.views.call import android.util.Log import chat.simplex.app.TAG import chat.simplex.app.model.ChatModel +import chat.simplex.app.views.helpers.ModalManager import chat.simplex.app.views.helpers.withApi +import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.minutes class CallManager(val chatModel: ChatModel) { fun reportNewIncomingCall(invitation: CallInvitation) { Log.d(TAG, "CallManager.reportNewIncomingCall") with (chatModel) { callInvitations[invitation.contact.id] = invitation - activeCallInvitation.value = invitation - controller.ntfManager.notifyCallInvitation(invitation) + if (Clock.System.now() - invitation.callTs <= 3.minutes) { + activeCallInvitation.value = invitation + controller.ntfManager.notifyCallInvitation(invitation) + } else { + val contact = invitation.contact + controller.ntfManager.notifyMessageReceived(chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText) + } } } fun acceptIncomingCall(invitation: CallInvitation) { + ModalManager.shared.closeModals() val call = chatModel.activeCall.value if (call == null) { justAcceptIncomingCall(invitation = invitation) @@ -41,7 +50,8 @@ class CallManager(val chatModel: ChatModel) { sharedKey = invitation.sharedKey ) showCallView.value = true - callCommand.value = WCallCommand.Start (media = invitation.peerMedia, aesKey = invitation.sharedKey) + val useRelay = controller.appPrefs.webrtcPolicyRelay.get() + callCommand.value = WCallCommand.Start (media = invitation.peerMedia, aesKey = invitation.sharedKey, relay = useRelay) callInvitations.remove(invitation.contact.id) if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { activeCallInvitation.value = null diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt index 9830f685c1..340acd96c0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt @@ -39,14 +39,10 @@ import kotlinx.serialization.encodeToString @Composable fun ActiveCallView(chatModel: ChatModel) { - val endCall = { - Log.d(TAG, "ActiveCallView: endCall") - chatModel.activeCall.value = null - chatModel.activeCallInvitation.value = null - chatModel.callCommand.value = null - chatModel.showCallView.value = false - } - BackHandler(onBack = endCall) + BackHandler(onBack = { + val call = chatModel.activeCall.value + if (call != null) withApi { chatModel.callManager.endCall(call) } + }) Box(Modifier.fillMaxSize()) { WebRTCView(chatModel.callCommand) { apiMsg -> Log.d(TAG, "received from WebRTCView: $apiMsg") @@ -85,7 +81,8 @@ fun ActiveCallView(chatModel: ChatModel) { } is WCallResponse.Ended -> { chatModel.activeCall.value = call.copy(callState = CallState.Ended) - endCall() + withApi { chatModel.callManager.endCall(call) } + chatModel.showCallView.value = false } is WCallResponse.Ok -> when (val cmd = apiMsg.command) { is WCallCommand.Answer -> @@ -102,7 +99,8 @@ fun ActiveCallView(chatModel: ChatModel) { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = false) } } - is WCallCommand.End -> endCall() + is WCallCommand.End -> + chatModel.showCallView.value = false else -> {} } is WCallResponse.Error -> { @@ -112,21 +110,15 @@ fun ActiveCallView(chatModel: ChatModel) { } } val call = chatModel.activeCall.value - if (call != null) ActiveCallOverlay(call, chatModel, endCall) + if (call != null) ActiveCallOverlay(call, chatModel) } } @Composable -private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, endCall: () -> Unit) { +private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) { ActiveCallOverlayLayout( call = call, - dismiss = { - chatModel.callCommand.value = WCallCommand.End - withApi { - chatModel.controller.apiEndCall(call.contact) - endCall() - } - }, + dismiss = { withApi { chatModel.callManager.endCall(call) } }, toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) }, toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) }, flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt index 54b6d20351..1a185daca6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt @@ -30,12 +30,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.* import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.Contact +import chat.simplex.app.model.* import chat.simplex.app.model.NtfManager.Companion.OpenChatAction import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.app.views.onboarding.SimpleXLogo +import kotlinx.datetime.Clock class IncomingCallActivity: ComponentActivity() { private val vm by viewModels() @@ -115,12 +115,12 @@ fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel val cm = chatModel.callManager val cxt = LocalContext.current val scope = rememberCoroutineScope() - var acceptCallsFromLockScreen by remember { mutableStateOf(chatModel.controller.prefAcceptCallsFromLockScreen.get()) } + var callOnLockScreen by remember { mutableStateOf(chatModel.controller.appPrefs.callOnLockScreen.get()) } LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) } DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } } IncomingCallLockScreenAlertLayout( invitation, - acceptCallsFromLockScreen, + callOnLockScreen, rejectCall = { cm.endCall(invitation = invitation) }, ignoreCall = { chatModel.activeCallInvitation.value = null }, acceptCall = { cm.acceptIncomingCall(invitation = invitation) }, @@ -141,7 +141,7 @@ fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel @Composable fun IncomingCallLockScreenAlertLayout( invitation: CallInvitation, - acceptCallsFromLockScreen: Boolean, + callOnLockScreen: CallOnLockScreen?, rejectCall: () -> Unit, ignoreCall: () -> Unit, acceptCall: () -> Unit, @@ -155,7 +155,7 @@ fun IncomingCallLockScreenAlertLayout( ) { IncomingCallInfo(invitation) Spacer(Modifier.fillMaxHeight().weight(1f)) - if (acceptCallsFromLockScreen) { + if (callOnLockScreen == CallOnLockScreen.ACCEPT) { ProfileImage(size = 192.dp, image = invitation.contact.profile.image) Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2) Spacer(Modifier.fillMaxHeight().weight(1f)) @@ -166,7 +166,7 @@ fun IncomingCallLockScreenAlertLayout( Spacer(Modifier.size(48.dp)) LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) } - } else { + } else if (callOnLockScreen == CallOnLockScreen.SHOW) { SimpleXLogo() Text(stringResource(R.string.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp) Text(stringResource(R.string.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp) @@ -212,9 +212,10 @@ fun PreviewIncomingCallLockScreenAlert() { invitation = CallInvitation( contact = Contact.sampleData, peerMedia = CallMediaType.Audio, - sharedKey = null + sharedKey = null, + callTs = Clock.System.now() ), - acceptCallsFromLockScreen = false, + callOnLockScreen = null, rejectCall = {}, ignoreCall = {}, acceptCall = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt index 269381281e..703fea35bd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt @@ -22,6 +22,7 @@ import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Contact import chat.simplex.app.ui.theme.* import chat.simplex.app.views.usersettings.ProfilePreview +import kotlinx.datetime.Clock @Composable fun IncomingCallAlertView(invitation: CallInvitation, chatModel: ChatModel) { @@ -97,7 +98,8 @@ fun PreviewIncomingCallAlertLayout() { invitation = CallInvitation( contact = Contact.sampleData, peerMedia = CallMediaType.Audio, - sharedKey = null + sharedKey = null, + callTs = Clock.System.now() ), rejectCall = {}, ignoreCall = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt index f2a8173bf7..2c8919f3b8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringResource import chat.simplex.app.R import chat.simplex.app.model.Contact import chat.simplex.app.views.helpers.generalGetString +import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -89,7 +90,7 @@ sealed class WCallResponse { @Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String) @Serializable class WebRTCExtraInfo(val rtcIceCandidates: String) @Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities) -@Serializable class CallInvitation(val contact: Contact, val peerMedia: CallMediaType, val sharedKey: String?) { +@Serializable class CallInvitation(val contact: Contact, val peerMedia: CallMediaType, val sharedKey: String?, val callTs: Instant) { val callTypeText: String get() = generalGetString(when(peerMedia) { CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 4ac07f6dd4..c7252a5dae 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -66,10 +66,8 @@ fun scaffoldController(): ScaffoldController { @Composable fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { val scaffoldCtrl = scaffoldController() - if (chatModel.clearOverlays.value) { - scaffoldCtrl.collapse() - ModalManager.shared.closeModal() - chatModel.clearOverlays.value = false + LaunchedEffect(chatModel.clearOverlays.value) { + if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse() } BottomSheetScaffold( scaffoldState = scaffoldCtrl.state, 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 6349354c84..ea720b631f 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 @@ -48,6 +48,10 @@ class ModalManager { modalCount.value = modalViews.count() } + fun closeModals() { + while (modalViews.isNotEmpty()) closeModal() + } + @Composable fun showInView() { if (modalCount.value > 0) modalViews.lastOrNull()?.invoke(::closeModal) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt index 2176249aac..5037647184 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt @@ -5,7 +5,10 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import chat.simplex.app.R import chat.simplex.app.model.* @@ -14,28 +17,38 @@ import chat.simplex.app.ui.theme.HighOrLowlight @Composable fun CallSettingsView(m: ChatModel) { CallSettingsLayout( - webrtcPolicyRelay = m.controller.prefWebrtcPolicyRelay, - acceptCallsFromLockScreen = m.controller.prefAcceptCallsFromLockScreen + webrtcPolicyRelay = m.controller.appPrefs.webrtcPolicyRelay, + callOnLockScreen = m.controller.appPrefs.callOnLockScreen ) } @Composable fun CallSettingsLayout( webrtcPolicyRelay: Preference, - acceptCallsFromLockScreen: Preference, + callOnLockScreen: Preference, ) { Column( Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(8.dp) ) { + val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } Text( stringResource(R.string.call_settings), Modifier.padding(bottom = 24.dp), style = MaterialTheme.typography.h1 ) SharedPreferenceToggle(stringResource(R.string.connect_calls_via_relay), webrtcPolicyRelay) - SharedPreferenceToggle(stringResource(R.string.accept_calls_from_lock_screen), acceptCallsFromLockScreen) + Column { + Text(stringResource(R.string.call_on_lock_screen)) + Row { + SharedPreferenceRadioButton(stringResource(R.string.no_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.DISABLE) + Spacer(Modifier.fillMaxWidth().weight(1f)) + SharedPreferenceRadioButton(stringResource(R.string.show_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.SHOW) + Spacer(Modifier.fillMaxWidth().weight(1f)) + SharedPreferenceRadioButton(stringResource(R.string.accept_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.ACCEPT) + } + } } } @@ -61,3 +74,15 @@ fun SharedPreferenceToggle( ) } } + +@Composable +fun SharedPreferenceRadioButton(text: String, prefState: MutableState, preference: Preference, value: T) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text) + val colors = RadioButtonDefaults.colors(selectedColor = MaterialTheme.colors.primary) + RadioButton(selected = prefState.value == value, colors = colors, onClick = { + preference.set(value) + prefState.value = value + }) + } +} 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 30fedfb726..acbeb5b237 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 @@ -34,9 +34,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { val user = chatModel.currentUser.value fun setRunServiceInBackground(on: Boolean) { - chatModel.controller.prefRunServiceInBackground.set(on) + chatModel.controller.appPrefs.runServiceInBackground.set(on) if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) { - chatModel.controller.prefBackgroundServiceNoticeShown.set(false) + chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false) } chatModel.controller.showBackgroundServiceNoticeIfNeeded() chatModel.runServiceInBackground.value = on @@ -135,6 +135,7 @@ fun SettingsLayout( Icon( Icons.Outlined.QrCode, contentDescription = stringResource(R.string.icon_descr_address), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text(stringResource(R.string.your_simplex_contact_address)) @@ -146,6 +147,7 @@ fun SettingsLayout( Icon( Icons.Outlined.Videocam, contentDescription = stringResource(R.string.call_settings), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text(stringResource(R.string.call_settings)) @@ -157,6 +159,7 @@ fun SettingsLayout( Icon( Icons.Outlined.HelpOutline, contentDescription = stringResource(R.string.icon_descr_help), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text(stringResource(R.string.how_to_use_simplex_chat)) @@ -168,6 +171,7 @@ fun SettingsLayout( Icon( Icons.Outlined.Info, contentDescription = stringResource(R.string.icon_descr_help), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text(stringResource(R.string.about_simplex_chat)) @@ -179,6 +183,7 @@ fun SettingsLayout( Icon( Icons.Outlined.TextFormat, contentDescription = stringResource(R.string.markdown_help), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text(stringResource(R.string.markdown_in_messages)) @@ -190,6 +195,7 @@ fun SettingsLayout( Icon( Icons.Outlined.Tag, contentDescription = stringResource(R.string.icon_descr_simplex_team), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text( @@ -204,6 +210,7 @@ fun SettingsLayout( Icon( Icons.Outlined.Email, contentDescription = stringResource(R.string.icon_descr_email), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text( @@ -218,6 +225,7 @@ fun SettingsLayout( Icon( Icons.Outlined.Dns, contentDescription = stringResource(R.string.smp_servers), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text(stringResource(R.string.smp_servers)) @@ -233,6 +241,7 @@ fun SettingsLayout( Icon( Icons.Outlined.Bolt, contentDescription = stringResource(R.string.private_notifications), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text( @@ -261,6 +270,7 @@ fun SettingsLayout( Icon( Icons.Outlined.Lock, contentDescription = stringResource(R.string.chat_lock), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text( @@ -287,6 +297,7 @@ fun SettingsLayout( Icon( painter = painterResource(id = R.drawable.ic_outline_terminal), contentDescription = stringResource(R.string.chat_console), + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text(stringResource(R.string.chat_console)) @@ -298,6 +309,7 @@ fun SettingsLayout( Icon( painter = painterResource(id = R.drawable.ic_github), contentDescription = "GitHub", + tint = HighOrLowlight, ) Spacer(Modifier.padding(horizontal = 4.dp)) Text(annotatedStringResource(R.string.install_simplex_chat_for_terminal)) diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index b339b2e153..f8b1242652 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -380,12 +380,15 @@ Настройки звонков Соединяться через сервер (relay) - Принимать с экрана блокировки + Звонки на экране блокировки: + Принимать + Показывать + Выключить - Open SimpleX Chat\nto accept call - You can allow accepting calls from lock screen via Settings. - Open + Откройте SimpleX Chat\nчтобы принять звонок + Вы можете разрешить принимать звонки на экране блокировки через Настройки. + Открыть e2e зашифровано diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index ab91313ff0..cb1d5cc80e 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -382,7 +382,10 @@ Call settings Connect via relay - Accept calls from lock screen + Calls on lock screen: + Accept + Show + Disable Open SimpleX Chat to accept call diff --git a/apps/ios/Shared/Model/Shared/APITypes.swift b/apps/ios/Shared/Model/Shared/APITypes.swift index 2a651222e1..c4e5f5eabe 100644 --- a/apps/ios/Shared/Model/Shared/APITypes.swift +++ b/apps/ios/Shared/Model/Shared/APITypes.swift @@ -199,7 +199,7 @@ enum ChatResponse: Decodable, Error { case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileRcvCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndGroupFileCancelled(chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer]) - case callInvitation(contact: Contact, callType: CallType, sharedKey: String?) + case callInvitation(contact: Contact, callType: CallType, sharedKey: String?, callTs: Date) case callOffer(contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) case callAnswer(contact: Contact, answer: WebRTCSession) case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo) @@ -322,7 +322,7 @@ enum ChatResponse: Decodable, Error { case let .sndFileCancelled(chatItem, _): return String(describing: chatItem) case let .sndFileRcvCancelled(chatItem, _): return String(describing: chatItem) case let .sndGroupFileCancelled(chatItem, _, _): return String(describing: chatItem) - case let .callInvitation(contact, callType, sharedKey): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")" + case let .callInvitation(contact, callType, sharedKey, _): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")" case let .callOffer(contact, callType, offer, sharedKey, askConfirmation): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))" case let .callAnswer(contact, answer): return "contact: \(contact.id)\nanswer: \(String(describing: answer))" case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))" diff --git a/apps/ios/Shared/Model/Shared/CallTypes.swift b/apps/ios/Shared/Model/Shared/CallTypes.swift index 402387055f..acb14f2199 100644 --- a/apps/ios/Shared/Model/Shared/CallTypes.swift +++ b/apps/ios/Shared/Model/Shared/CallTypes.swift @@ -28,6 +28,7 @@ struct CallInvitation { var callkitUUID: UUID? var peerMedia: CallMediaType var sharedKey: String? + var callTs: Date var callTypeText: LocalizedStringKey { get { switch peerMedia { @@ -39,7 +40,8 @@ struct CallInvitation { static let sampleData = CallInvitation( contact: Contact.sampleData, - peerMedia: .audio + peerMedia: .audio, + callTs: .now ) } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index aa3ce27e6f..56edef0d2d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -629,9 +629,9 @@ func processReceivedMsg(_ res: ChatResponse) { let fileName = cItem.file?.filePath { removeFile(fileName) } - case let .callInvitation(contact, callType, sharedKey): + case let .callInvitation(contact, callType, sharedKey, callTs): let uuid = UUID() - var invitation = CallInvitation(contact: contact, callkitUUID: uuid, peerMedia: callType.media, sharedKey: sharedKey) + var invitation = CallInvitation(contact: contact, callkitUUID: uuid, peerMedia: callType.media, sharedKey: sharedKey, callTs: callTs) m.callInvitations[contact.id] = invitation CallController.shared.reportNewIncomingCall(invitation: invitation) { error in if let error = error { diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index ecd7633ec1..008db03e9f 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -127,7 +127,9 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject { provider.reportNewIncomingCall(with: uuid, update: update, completion: completion) } else { NtfManager.shared.notifyCallInvitation(invitation) - activeCallInvitation = invitation + if invitation.callTs.timeIntervalSinceNow >= -180 { + activeCallInvitation = invitation + } } } diff --git a/apps/ios/Shared/Views/UserSettings/CallSettings.swift b/apps/ios/Shared/Views/UserSettings/CallSettings.swift index ddcdb7cd6f..516b237538 100644 --- a/apps/ios/Shared/Views/UserSettings/CallSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/CallSettings.swift @@ -12,8 +12,31 @@ struct CallSettings: View { @AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true var body: some View { - List { - Toggle("Connect via relay", isOn: $webrtcPolicyRelay) + VStack { + List { + Section("Settings") { + Toggle("Connect via relay", isOn: $webrtcPolicyRelay) + } + + Section("Limitations") { + VStack(alignment: .leading, spacing: 8) { + textListItem("1.", "Do NOT use SimpleX for emergency calls.") + textListItem("2.", "Pre-arrange the calls, as notifications arrive with a delay (we are improving it).") + textListItem("3.", "The microphone does not work when the app is in the background.") + textListItem("4.", "To prevent the call interruption, enable Do Not Disturb mode.") + textListItem("5.", "If the video fails to connect, flip the camera to resolve it.") + } + .font(.callout) + .padding(.vertical, 8) + } + } + } + } + + private func textListItem(_ n: String, _ text: LocalizedStringKey) -> some View { + ZStack(alignment: .topLeading) { + Text(n) + Text(text).frame(maxWidth: .infinity, alignment: .leading).padding(.leading, 20) } } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 9608676742..214e9182f0 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -119,6 +119,7 @@ struct SettingsView: View { Image(colorScheme == .dark ? "github_light" : "github") .resizable() .frame(width: 24, height: 24) + .opacity(0.5) Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)") .padding(.leading, indent) } @@ -128,12 +129,7 @@ struct SettingsView: View { // notificationsToggle(token) // } // } -// NavigationLink { -// CallViewDebug() -// .frame(maxHeight: .infinity, alignment: .top) -// } label: { - Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") -// } + Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") } } .navigationTitle("Your settings") @@ -142,7 +138,7 @@ struct SettingsView: View { private func settingsRow(_ icon: String, content: @escaping () -> Content) -> some View { ZStack(alignment: .leading) { - Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center) + Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.secondary) content().padding(.leading, indent) } }