From 387aec859356286b88b6e329112da6c794763ff7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 27 May 2022 08:43:15 +0100 Subject: [PATCH] android: webrtc calls notification and alert (#699) * android: webrtc calls notification and alert * add ringtone to incoming call * incoming call on full screen * enable notification ringtone * remove text * use translated strings in call notification --- apps/android/app/src/main/AndroidManifest.xml | 4 + .../java/chat/simplex/app/MainActivity.kt | 29 ++- .../main/java/chat/simplex/app/SimplexApp.kt | 3 +- .../java/chat/simplex/app/model/ChatModel.kt | 24 +-- .../java/chat/simplex/app/model/NtfManager.kt | 124 ++++++----- .../java/chat/simplex/app/model/SimpleXAPI.kt | 75 +++---- .../java/chat/simplex/app/ui/theme/Color.kt | 4 +- .../simplex/app/views/call/CallManager.kt | 90 ++++++++ .../chat/simplex/app/views/call/CallView.kt | 34 ++- .../app/views/call/IncomingCallActivity.kt | 201 ++++++++++++++++++ .../app/views/call/IncomingCallAlertView.kt | 107 ++++++++++ .../simplex/app/views/call/SoundPlayer.kt | 39 ++++ .../chat/simplex/app/views/call/WebRTC.kt | 16 +- .../chat/simplex/app/views/chat/ChatView.kt | 10 +- .../{CICallViewItem.kt => CICallItemView.kt} | 7 +- .../app/views/helpers/ChatInfoImage.kt | 6 +- .../chat/simplex/app/views/helpers/Util.kt | 6 +- .../app/views/onboarding/SimpleXInfo.kt | 19 +- .../app/views/usersettings/SettingsView.kt | 28 +-- .../app/src/main/res/drawable-hdpi/icon.png | Bin 0 -> 8996 bytes .../app/src/main/res/raw/ring_once.mp3 | Bin 0 -> 72269 bytes .../app/src/main/res/values-ru/strings.xml | 10 +- .../app/src/main/res/values/strings.xml | 22 +- apps/ios/Shared/Views/Call/CallManager.swift | 4 +- apps/ios/Shared/Views/Call/WebRTCView.swift | 138 ++++++------ 25 files changed, 727 insertions(+), 273 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt rename apps/android/app/src/main/java/chat/simplex/app/views/chat/item/{CICallViewItem.kt => CICallItemView.kt} (96%) create mode 100644 apps/android/app/src/main/res/drawable-hdpi/icon.png create mode 100644 apps/android/app/src/main/res/raw/ring_once.mp3 diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index 7cbf6ab6bd..d75a79b49f 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + + + SplashView() - onboarding == OnboardingStage.OnboardingComplete && userCreated -> - if (chatModel.showCallView.value) ActiveCallView(chatModel) - else if (chatModel.chatId.value == null) ChatListView(chatModel) - else ChatView(chatModel) - onboarding == OnboardingStage.Step1_SimpleXInfo -> + onboarding == OnboardingStage.OnboardingComplete && userCreated -> { + Box { + if (chatModel.showCallView.value) ActiveCallView(chatModel) + else if (chatModel.chatId.value == null) ChatListView(chatModel) + else ChatView(chatModel) + + val invitation = chatModel.activeCallInvitation.value + if (invitation != null) IncomingCallAlertView(invitation, chatModel) + } + } onboarding == OnboardingStage.Step1_SimpleXInfo -> Box(Modifier.padding(horizontal = 20.dp)) { SimpleXInfo(chatModel, onboarding = true) } @@ -122,6 +129,16 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) { chatModel.chatId.value = null chatModel.clearOverlays.value = true } + NtfManager.AcceptCallAction -> { + val chatId = intent.getStringExtra("chatId") + Log.d(TAG, "processNotificationIntent: AcceptCallAction $chatId") + val invitation = chatModel.callInvitations[chatId] + if (invitation == null) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended)) + } else { + chatModel.callManager.acceptIncomingCall(invitation = invitation) + } + } } } 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 9559f9fd70..bdb8fb5d09 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 @@ -5,8 +5,7 @@ import android.net.LocalServerSocket import android.util.Log import androidx.lifecycle.* import chat.simplex.app.model.* -import chat.simplex.app.views.helpers.getFilesDirectory -import chat.simplex.app.views.helpers.withApi +import chat.simplex.app.views.helpers.* import chat.simplex.app.views.onboarding.OnboardingStage import java.io.BufferedReader import java.io.InputStreamReader 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 7f7c8ec89c..824701dae4 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,8 +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.R -import chat.simplex.app.ui.theme.SecretColor -import chat.simplex.app.ui.theme.SimplexBlue +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.call.* import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.onboarding.OnboardingStage @@ -41,11 +40,13 @@ class ChatModel(val controller: ChatController) { val runServiceInBackground = mutableStateOf(true) // current WebRTC call + val callManager = CallManager(this) val callInvitations = mutableStateMapOf() - val activeCallInvitation = mutableStateOf(null) + val activeCallInvitation = mutableStateOf(null) val activeCall = mutableStateOf(null) val callCommand = mutableStateOf(null) val showCallView = mutableStateOf(false) + val switchingCall = mutableStateOf(false) fun updateUserProfile(profile: Profile) { val user = currentUser.value @@ -457,10 +458,10 @@ class Connection(val connId: Long, val connStatus: ConnStatus) { @Serializable class Profile( - val displayName: String, - val fullName: String, - val image: String? = null - ) { + override val displayName: String, + override val fullName: String, + override val image: String? = null +): NamedChat { companion object { val sampleData = Profile( displayName = "alice", @@ -687,13 +688,6 @@ data class ChatItem ( if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.memberProfile.displayName else null - val isMsgContent: Boolean get() = - when (content) { - is CIContent.SndMsgContent -> true - is CIContent.RcvMsgContent -> true - else -> false - } - val isDeletedContent: Boolean get() = when (content) { is CIContent.SndDeleted -> true @@ -1080,7 +1074,7 @@ enum class FormatColor(val color: String) { val uiColor: Color @Composable get() = when (this) { red -> Color.Red - green -> Color.Green + green -> SimplexGreen blue -> SimplexBlue yellow -> Color.Yellow cyan -> Color.Cyan 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 1b7aa8438d..0ba0004d71 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,14 +1,17 @@ package chat.simplex.app.model import android.app.* -import android.content.Context -import android.content.Intent +import android.content.* +import android.graphics.BitmapFactory +import android.media.AudioAttributes +import android.net.Uri import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import chat.simplex.app.* -import chat.simplex.app.views.call.CallInvitation -import chat.simplex.app.views.call.CallMediaType +import chat.simplex.app.views.call.* +import chat.simplex.app.views.helpers.base64ToBitmap +import chat.simplex.app.views.helpers.generalGetString import kotlinx.datetime.Clock class NtfManager(val context: Context) { @@ -17,6 +20,12 @@ class NtfManager(val context: Context) { const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION" const val OpenChatAction: String = "chat.simplex.app.OPEN_CHAT" const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS" + + // DO NOT change notification channel settings / names + const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION" + const val LockScreenCallChannel: String = "chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION" + const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL" + const val CallNotificationId: Int = -1 } private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -24,11 +33,22 @@ class NtfManager(val context: Context) { private val msgNtfTimeoutMs = 30000L init { - manager.createNotificationChannel(NotificationChannel( - MessageChannel, - "SimpleX Chat messages", - NotificationManager.IMPORTANCE_HIGH - )) + manager.createNotificationChannel(NotificationChannel(MessageChannel, "SimpleX Chat messages", NotificationManager.IMPORTANCE_HIGH)) + manager.createNotificationChannel(NotificationChannel(LockScreenCallChannel, "SimpleX Chat calls (lock screen)", NotificationManager.IMPORTANCE_HIGH)) + manager.createNotificationChannel(callNotificationChannel()) + } + + private fun callNotificationChannel(): NotificationChannel { + val callChannel = NotificationChannel(CallChannel, "SimpleX Chat calls", NotificationManager.IMPORTANCE_HIGH) + val attrs = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build() + val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once) + Log.d(TAG,"callNotificationChannel sound: $soundUri") + callChannel.setSound(soundUri, attrs) + callChannel.enableVibration(true) + return callChannel } fun cancelNotificationsForChat(chatId: String) { @@ -58,7 +78,7 @@ class NtfManager(val context: Context) { .setSmallIcon(R.drawable.ntf_icon) .setColor(0x88FFFF) .setAutoCancel(true) - .setContentIntent(getMsgPendingIntent(cInfo.id)) + .setContentIntent(chatPendingIntent(OpenChatAction, cInfo.id)) .setSilent(recentNotification) .build() @@ -68,7 +88,7 @@ class NtfManager(val context: Context) { .setGroup(MessageGroup) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setGroupSummary(true) - .setContentIntent(getSummaryNtfIntent()) + .setContentIntent(chatPendingIntent(ShowChatsAction)) .build() with(NotificationManagerCompat.from(context)) { @@ -78,37 +98,51 @@ class NtfManager(val context: Context) { } } - fun notifyCallInvitation(contact: Contact, invitation: CallInvitation) { - Log.d(TAG, "notifyCallInvitationReceived ${contact.id}") - - val notification = NotificationCompat.Builder(context, MessageChannel) - .setContentTitle(contact.displayName) - .setContentText("Incoming ${invitation.peerMedia} call (${if (invitation.sharedKey == null) "not e2e encrypted" else "e2e encrypted"})") + fun notifyCallInvitation(invitation: CallInvitation) { + if (isAppOnForeground(context)) return + val contactId = invitation.contact.id + Log.d(TAG, "notifyCallInvitation $contactId") + val keyguardManager = getKeyguardManager(context) + val image = invitation.contact.image + var ntfBuilder = + if (keyguardManager.isDeviceLocked) { + val fullScreenIntent = Intent(context, IncomingCallActivity::class.java) + val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT) + NotificationCompat.Builder(context, LockScreenCallChannel) + .setFullScreenIntent(fullScreenPendingIntent, true) + .setSilent(true) + } else { + val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once) + NotificationCompat.Builder(context, CallChannel) + .setContentIntent(chatPendingIntent(OpenChatAction, invitation.contact.id)) + .addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, contactId)) + .setSound(soundUri) + } + val text = generalGetString( + if (invitation.peerMedia == CallMediaType.Video) { + if (invitation.sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call + } else { + if (invitation.sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call + } + ) + ntfBuilder = ntfBuilder + .setContentTitle(invitation.contact.displayName) + .setContentText(text) .setPriority(NotificationCompat.PRIORITY_HIGH) - .setGroup(MessageGroup) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setCategory(NotificationCompat.CATEGORY_CALL) .setSmallIcon(R.drawable.ntf_icon) + .setLargeIcon(if (image == null) BitmapFactory.decodeResource(context.resources, R.drawable.icon) else base64ToBitmap(image)) .setColor(0x88FFFF) .setAutoCancel(true) - .setContentIntent(getMsgPendingIntent(contact.id)) - .setSilent(false) - .build() - -// val summary = NotificationCompat.Builder(context, MessageChannel) -// .setSmallIcon(R.drawable.ntf_icon) -// .setColor(0x88FFFF) -// .setGroup(MessageGroup) -// .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) -// .setGroupSummary(true) -// .setContentIntent(getSummaryNtfIntent()) -// .build() - with(NotificationManagerCompat.from(context)) { - notify(0, notification) -// notify(0, summary) + notify(CallNotificationId, ntfBuilder.build()) } } + fun cancelCallNotification() { + manager.cancel(CallNotificationId) + } + private fun hideSecrets(cItem: ChatItem) : String { val md = cItem.formattedText return if (md == null) { @@ -126,25 +160,13 @@ class NtfManager(val context: Context) { } } - private fun getMsgPendingIntent(chatId: String) : PendingIntent{ - Log.d(TAG, "getMsgPendingIntent $chatId") + private fun chatPendingIntent(intentAction: String, chatId: String? = null): PendingIntent { + Log.d(TAG, "chatPendingIntent for $intentAction") val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt() - val intent = Intent(context, MainActivity::class.java) + var intent = Intent(context, MainActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra("chatId", chatId) - .setAction(OpenChatAction) - return TaskStackBuilder.create(context).run { - addNextIntentWithParentStack(intent) - getPendingIntent(uniqueInt, PendingIntent.FLAG_IMMUTABLE) - } - } - - private fun getSummaryNtfIntent() : PendingIntent{ - Log.d(TAG, "getSummaryNtfIntent") - 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) - .setAction(ShowChatsAction) + .setAction(intentAction) + if (chatId != null) intent = intent.putExtra("chatId", chatId) return TaskStackBuilder.create(context).run { addNextIntentWithParentStack(intent) getPendingIntent(uniqueInt, PendingIntent.FLAG_IMMUTABLE) 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 21f5722e0c..6c525e531a 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 @@ -23,8 +23,7 @@ import chat.simplex.app.R import chat.simplex.app.views.call.* import chat.simplex.app.views.helpers.* import chat.simplex.app.views.onboarding.OnboardingStage -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.* @@ -34,6 +33,18 @@ import kotlin.concurrent.thread typealias ChatCtrl = Long +fun isAppOnForeground(context: Context): Boolean { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val appProcesses = activityManager.runningAppProcesses ?: return false + val packageName = context.packageName + for (appProcess in appProcesses) { + if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) { + return true + } + } + 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) @@ -69,22 +80,10 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager fun startReceiver() { Log.d(TAG, "ChatController startReceiver") thread(name="receiver") { - withApi { recvMspLoop() } + GlobalScope.launch { withContext(Dispatchers.IO) { recvMspLoop() } } } } - open fun isAppOnForeground(context: Context): Boolean { - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val appProcesses = activityManager.runningAppProcesses ?: return false - val packageName = context.packageName - for (appProcess in appProcesses) { - if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) { - return true - } - } - return false - } - suspend fun sendCmd(cmd: CC): CR { return withContext(Dispatchers.IO) { val c = cmd.cmdString @@ -454,7 +453,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } } - if (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id) { + if (!cItem.isCall && (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id)) { ntfManager.notifyMessageReceived(cInfo, cItem) } } @@ -501,36 +500,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } is CR.CallInvitation -> { - val invitation = CallInvitation(r.callType.media, r.sharedKey) - chatModel.callInvitations[r.contact.id] = invitation - if (chatModel.activeCallInvitation.value == null) { - chatModel.activeCallInvitation.value = ContactRef(r.contact.apiId, r.contact.localDisplayName) - } - ntfManager.notifyCallInvitation(r.contact, invitation) - AlertManager.shared.showAlertDialog( - title = invitation.callTitle, - text = String.format(generalGetString(R.string.contact_wants_to_connect_via_call), r.contact.displayName) + " " + invitation.callTypeText + ".", - confirmText = generalGetString(R.string.answer), - onConfirm = { - if (chatModel.activeCallInvitation.value == null) { - AlertManager.shared.hideAlert() - withApi { AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended)) } - } else { - chatModel.activeCallInvitation.value = null - chatModel.activeCall.value = Call( - contact = r.contact, - callState = CallState.InvitationReceived, - localMedia = invitation.peerMedia, - sharedKey = invitation.sharedKey - ) - chatModel.callCommand.value = WCallCommand.Start(media = invitation.peerMedia, aesKey = invitation.sharedKey) - chatModel.showCallView.value = true - } - }, - onDismiss = { - chatModel.activeCallInvitation.value = null - } - ) + val invitation = CallInvitation(r.contact, r.callType.media, r.sharedKey) + chatModel.callManager.reportNewIncomingCall(invitation) } is CR.CallOffer -> { // TODO askConfirmation? @@ -542,7 +513,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } is CR.CallAnswer -> { withCall(r, r.contact) { call -> - chatModel.activeCall.value = call.copy(callState = CallState.Negotiated) + chatModel.activeCall.value = call.copy(callState = CallState.AnswerReceived) chatModel.callCommand.value = WCallCommand.Answer(answer = r.answer.rtcSession, iceCandidates = r.answer.rtcIceCandidates) } } @@ -552,15 +523,17 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } is CR.CallEnded -> { - withCall(r, r.contact) { _ -> + val invitation = chatModel.callInvitations.remove(r.contact.id) + if (invitation != null) { + chatModel.callManager.reportCallRemoteEnded(invitation = invitation) + } + withCall(r, r.contact) { call -> chatModel.callCommand.value = WCallCommand.End withApi { chatModel.activeCall.value = null - chatModel.callCommand.value = null + chatModel.showCallView.value = false } } - chatModel.activeCallInvitation.value = null - chatModel.showCallView.value = false } else -> Log.d(TAG , "unsupported event: ${r.responseType}") diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt index 262279053e..14e7f20054 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -8,7 +8,7 @@ val Purple700 = Color(0xFF3700B3) val Teal200 = Color(0xFF03DAC5) val Gray = Color(0x22222222) val SimplexBlue = Color(0, 136, 255, 255) // If this value changes also need to update #0088ff in string resource files -val SimplexGreen = Color(98, 196, 103, 255) +val SimplexGreen = Color(77, 218, 103, 255) val SecretColor = Color(0x40808080) val LightGray = Color(241, 242, 246, 255) val DarkGray = Color(43, 44, 46, 255) @@ -17,6 +17,8 @@ val MessagePreviewDark = Color(179, 175, 174, 255) val MessagePreviewLight = Color(49, 45, 44, 255) val ToolbarLight = Color(220, 220, 220, 20) val ToolbarDark = Color(80, 80, 80, 20) +val IncomingCallLight = Color(239, 237, 236, 255) +val IncomingCallDark = Color(34, 30, 29, 255) val WarningOrange = Color(255, 127, 0, 255) val FileLight = Color(183, 190, 199, 255) val FileDark = Color(101, 101, 106, 255) 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 new file mode 100644 index 0000000000..13bad493b1 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt @@ -0,0 +1,90 @@ +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.withApi + +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) + } + } + + fun acceptIncomingCall(invitation: CallInvitation) { + val call = chatModel.activeCall.value + if (call == null) { + justAcceptIncomingCall(invitation = invitation) + } else { + withApi { + chatModel.switchingCall.value = true + try { + endCall(call = call) + justAcceptIncomingCall(invitation = invitation) + } finally { + withApi { chatModel.switchingCall.value = false } + } + } + } + } + + private fun justAcceptIncomingCall(invitation: CallInvitation) { + with (chatModel) { + activeCall.value = Call( + contact = invitation.contact, + callState = CallState.InvitationAccepted, + localMedia = invitation.peerMedia, + sharedKey = invitation.sharedKey + ) + showCallView.value = true + callCommand.value = WCallCommand.Start (media = invitation.peerMedia, aesKey = invitation.sharedKey) + callInvitations.remove(invitation.contact.id) + if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { + activeCallInvitation.value = null + controller.ntfManager.cancelCallNotification() + } + } + } + + suspend fun endCall(call: Call) { + with (chatModel) { + if (call.callState == CallState.Ended) { + Log.d(TAG, "CallManager.endCall: call ended") + activeCall.value = null + showCallView.value = false + } else { + Log.d(TAG, "CallManager.endCall: ending call...") + callCommand.value = WCallCommand.End + showCallView.value = false + controller.apiEndCall(call.contact) + activeCall.value = null + } + } + } + + fun endCall(invitation: CallInvitation) { + with (chatModel) { + callInvitations.remove(invitation.contact.id) + if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { + activeCallInvitation.value = null + controller.ntfManager.cancelCallNotification() + } + withApi { + if (!controller.apiRejectCall(invitation.contact)) { + Log.e(TAG, "apiRejectCall error") + } + } + } + } + + fun reportCallRemoteEnded(invitation: CallInvitation) { + if (chatModel.activeCallInvitation.value?.contact?.id == invitation.contact.id) { + chatModel.activeCallInvitation.value = null + chatModel.controller.ntfManager.cancelCallNotification() + } + } +} \ No newline at end of file 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 57a816eb3c..391ca33061 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 @@ -1,20 +1,12 @@ package chat.simplex.app.views.call import android.Manifest -import android.content.ClipData -import android.content.ClipboardManager -import android.graphics.fonts.FontStyle -import android.os.Build -import android.service.controls.templates.ControlButton import android.util.Log import android.view.ViewGroup import android.webkit.* import androidx.activity.compose.BackHandler -import androidx.annotation.RequiresApi import androidx.annotation.StringRes -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.magnifier import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -28,11 +20,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.capitalize import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.webkit.WebViewAssetLoader @@ -41,7 +31,6 @@ import chat.simplex.app.R import chat.simplex.app.TAG import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.chat.ChatInfoLayout import chat.simplex.app.views.helpers.* import com.google.accompanist.permissions.rememberMultiplePermissionsState import kotlinx.coroutines.delay @@ -94,15 +83,25 @@ fun ActiveCallView(chatModel: ChatModel) { is WCallResponse.Connected -> { chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) } - is WCallResponse.Ended -> endCall() + is WCallResponse.Ended -> { + chatModel.activeCall.value = call.copy(callState = CallState.Ended) + endCall() + } is WCallResponse.Ok -> when (val cmd = apiMsg.command) { + is WCallCommand.Answer -> + chatModel.activeCall.value = call.copy(callState = CallState.Negotiated) is WCallCommand.Media -> { when (cmd.media) { CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable) CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable) } } - is WCallCommand.Camera -> chatModel.activeCall.value = call.copy(localCamera = cmd.camera) + is WCallCommand.Camera -> { + chatModel.activeCall.value = call.copy(localCamera = cmd.camera) + if (!call.audioEnabled) { + chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = false) + } + } is WCallCommand.End -> endCall() else -> {} } @@ -153,10 +152,11 @@ private fun ActiveCallOverlayLayout( IconButton(onClick = dismiss) { Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) } - ControlButton(call, Icons.Filled.FlipCameraAndroid, R.string.icon_descr_flip_camera, flipCamera) if (call.videoEnabled) { + ControlButton(call, Icons.Filled.FlipCameraAndroid, R.string.icon_descr_flip_camera, flipCamera) ControlButton(call, Icons.Filled.Videocam, R.string.icon_descr_video_off, toggleVideo) } else { + Spacer(Modifier.size(40.dp)) ControlButton(call, Icons.Outlined.VideocamOff, R.string.icon_descr_video_on, toggleVideo) } } @@ -208,7 +208,7 @@ private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) { } @Composable -private fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { +fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { @Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) = Text(text, color = Color(0xFFFFFFD8), style = style) Column(horizontalAlignment = alignment) { @@ -273,8 +273,6 @@ private fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { //} @Composable -// for debugging -// fun WebRTCView(callCommand: MutableState, onResponse: (String) -> Unit) { fun WebRTCView(callCommand: MutableState, onResponse: (WVAPIMessage) -> Unit) { val webView = remember { mutableStateOf(null) } val permissionsState = rememberMultiplePermissionsState( @@ -359,8 +357,6 @@ fun WebRTCView(callCommand: MutableState, onResponse: (WVAPIMessa } } } - } else { - Text("NEED PERMISSIONS") } } 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 new file mode 100644 index 0000000000..0226a4dacd --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt @@ -0,0 +1,201 @@ +package chat.simplex.app.views.call + +import android.app.KeyguardManager +import android.content.* +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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.ui.theme.* +import chat.simplex.app.views.helpers.ProfileImage +import chat.simplex.app.views.onboarding.SimpleXLogo + +class IncomingCallActivity: ComponentActivity() { + private val vm by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val activity = this + setContent { IncomingCallActivityView(vm.chatModel, activity) } + unlockForIncomingCall() + } + + override fun onDestroy() { + super.onDestroy() + lockAfterIncomingCall() + } + + private fun unlockForIncomingCall() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true) + setTurnScreenOn(true) + } else { + window.addFlags(activityFlags) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + getKeyguardManager(this).requestDismissKeyguard(this, null) + } + } + + private fun lockAfterIncomingCall() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(false) + setTurnScreenOn(false) + } else { + window.clearFlags(activityFlags) + } + } + + companion object { + const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + } +} + +fun getKeyguardManager(context: Context): KeyguardManager = + context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + +@Composable +fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) { + val switchingCall = m.switchingCall.value + val invitation = m.activeCallInvitation.value + val call = m.activeCall.value + val showCallView = m.showCallView.value + LaunchedEffect(invitation, call, switchingCall, showCallView) { + if (!switchingCall && invitation == null && (!showCallView || call == null)) { + Log.d(TAG, "IncomingCallActivityView: finishing activity") + activity.finish() + } + } + SimpleXTheme(darkTheme = true) { + Surface( + Modifier + .background(MaterialTheme.colors.background) + .fillMaxSize()) { + if (showCallView) { + Box { + ActiveCallView(m) + if (invitation != null) IncomingCallAlertView(invitation, m) + } + } else if (invitation != null) { + IncomingCallLockScreenAlert(invitation, m) + } + } + } +} + +@Composable +fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel) { + val cm = chatModel.callManager + val cxt = LocalContext.current + val scope = rememberCoroutineScope() + LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) } + DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } } + IncomingCallLockScreenAlertLayout( + invitation, + rejectCall = { cm.endCall(invitation = invitation) }, + ignoreCall = { chatModel.activeCallInvitation.value = null }, + acceptCall = { cm.acceptIncomingCall(invitation = invitation) } + ) +} + +@Composable +fun IncomingCallLockScreenAlertLayout( + invitation: CallInvitation, + rejectCall: () -> Unit, + ignoreCall: () -> Unit, + acceptCall: () -> Unit +) { + Column( + Modifier + .padding(30.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + IncomingCallInfo(invitation) + Spacer( + Modifier + .fillMaxHeight() + .weight(1f)) + ProfileImage(size = 192.dp, image = invitation.contact.profile.image) + Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2) + Spacer( + Modifier + .fillMaxHeight() + .weight(1f)) + Row { + LockScreenCallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall) + Spacer(Modifier.size(48.dp)) + LockScreenCallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall) + Spacer(Modifier.size(48.dp)) + LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) + } + } +} + +@Composable +private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color, action: () -> Unit) { + Surface( + shape = RoundedCornerShape(10.dp), + color = Color.Transparent + ) { + Column( + Modifier.defaultMinSize(minWidth = 50.dp).padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + IconButton(action) { + Icon(icon, text, tint = color, modifier = Modifier.scale(1.75f)) + } + Spacer(Modifier.height(16.dp)) + Text(text, style = MaterialTheme.typography.body2, color = HighOrLowlight) + } + } +} + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true +) +@Composable +fun PreviewIncomingCallLockScreenAlert() { + SimpleXTheme(true) { + Surface(Modifier.background(MaterialTheme.colors.background).fillMaxSize()) { + IncomingCallLockScreenAlertLayout( + invitation = CallInvitation( + contact = Contact.sampleData, + peerMedia = CallMediaType.Audio, + sharedKey = 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 new file mode 100644 index 0000000000..269381281e --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt @@ -0,0 +1,107 @@ +package chat.simplex.app.views.call + +import androidx.annotation.StringRes +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +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 + +@Composable +fun IncomingCallAlertView(invitation: CallInvitation, chatModel: ChatModel) { + val cm = chatModel.callManager + val cxt = LocalContext.current + val scope = rememberCoroutineScope() + LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = !chatModel.showCallView.value) } + DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } } + IncomingCallAlertLayout( + invitation, + rejectCall = { cm.endCall(invitation = invitation) }, + ignoreCall = { chatModel.activeCallInvitation.value = null }, + acceptCall = { cm.acceptIncomingCall(invitation = invitation) } + ) +} + +@Composable +fun IncomingCallAlertLayout( + invitation: CallInvitation, + rejectCall: () -> Unit, + ignoreCall: () -> Unit, + acceptCall: () -> Unit +) { + val color = if (isSystemInDarkTheme()) IncomingCallDark else IncomingCallLight + Column(Modifier.background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) { + IncomingCallInfo(invitation) + Spacer(Modifier.height(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White) + Spacer(Modifier.fillMaxWidth().weight(1f)) + CallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall) + CallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall) + CallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) + } + } +} + +@Composable +fun IncomingCallInfo(invitation: CallInvitation) { + @Composable fun CallIcon(icon: ImageVector, descr: String) = Icon(icon, descr, tint = SimplexGreen) + Row { + if (invitation.peerMedia == CallMediaType.Video) CallIcon(Icons.Filled.Videocam, stringResource(R.string.icon_descr_video_call)) + else CallIcon(Icons.Filled.Phone, stringResource(R.string.icon_descr_audio_call)) + Spacer(Modifier.width(4.dp)) + Text(invitation.callTypeText) + } +} + +@Composable +private fun CallButton(text: String, icon: ImageVector, color: Color, action: () -> Unit) { + Surface( + shape = RoundedCornerShape(10.dp), + color = Color.Transparent + ) { + Column( + Modifier + .clickable(onClick = action) + .defaultMinSize(minWidth = 50.dp) + .padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon(icon, text, tint = color, modifier = Modifier.scale(1.2f)) + Text(text, style = MaterialTheme.typography.body2, color = HighOrLowlight) + } + } +} + +@Preview +@Composable +fun PreviewIncomingCallAlertLayout() { + SimpleXTheme { + IncomingCallAlertLayout( + invitation = CallInvitation( + contact = Contact.sampleData, + peerMedia = CallMediaType.Audio, + sharedKey = null + ), + rejectCall = {}, + ignoreCall = {}, + acceptCall = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt new file mode 100644 index 0000000000..f451330a7a --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt @@ -0,0 +1,39 @@ +package chat.simplex.app.views.call + +import android.content.Context +import android.media.MediaPlayer +import android.os.VibrationEffect +import android.os.Vibrator +import androidx.core.content.ContextCompat +import chat.simplex.app.R +import chat.simplex.app.views.helpers.withScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay + +class SoundPlayer { + var player: MediaPlayer? = null + var playing = false + + fun start(cxt: Context, scope: CoroutineScope, sound: Boolean) { + if (sound) player = MediaPlayer.create(cxt, R.raw.ring_once) + val vibrator = ContextCompat.getSystemService(cxt, Vibrator::class.java) + val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE) + playing = true + withScope(scope) { + while (playing) { + if (sound) player?.start() + vibrator?.vibrate(effect) + delay(3500) + } + } + } + + fun stop() { + playing = false + player?.stop() + } + + companion object { + val shared = SoundPlayer() + } +} \ No newline at end of file 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 a415710319..f2a8173bf7 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 @@ -26,7 +26,7 @@ data class Call( val encryptionStatus: String @Composable get() = when(callState) { CallState.WaitCapabilities -> "" CallState.InvitationSent -> stringResource(if (localEncrypted) R.string.status_e2e_encrypted else R.string.status_no_e2e_encryption) - CallState.InvitationReceived -> stringResource(if (sharedKey == null) R.string.status_contact_has_no_e2e_encryption else R.string.status_contact_has_e2e_encryption) + CallState.InvitationAccepted -> stringResource(if (sharedKey == null) R.string.status_contact_has_no_e2e_encryption else R.string.status_contact_has_e2e_encryption) else -> stringResource(if (!localEncrypted) R.string.status_no_e2e_encryption else if (sharedKey == null) R.string.status_contact_has_no_e2e_encryption else R.string.status_e2e_encrypted) } @@ -36,20 +36,24 @@ data class Call( enum class CallState { WaitCapabilities, InvitationSent, - InvitationReceived, + InvitationAccepted, OfferSent, OfferReceived, + AnswerReceived, Negotiated, - Connected; + Connected, + Ended; val text: String @Composable get() = when(this) { WaitCapabilities -> stringResource(R.string.callstate_starting) InvitationSent -> stringResource(R.string.callstate_waiting_for_answer) - InvitationReceived -> stringResource(R.string.callstate_starting) + InvitationAccepted -> stringResource(R.string.callstate_starting) OfferSent -> stringResource(R.string.callstate_waiting_for_confirmation) OfferReceived -> stringResource(R.string.callstate_received_answer) + AnswerReceived -> stringResource(R.string.callstate_received_confirmation) Negotiated -> stringResource(R.string.callstate_connecting) Connected -> stringResource(R.string.callstate_connected) + Ended -> stringResource(R.string.callstate_ended) } } @@ -85,7 +89,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 peerMedia: CallMediaType, val sharedKey: String?) { +@Serializable class CallInvitation(val contact: Contact, val peerMedia: CallMediaType, val sharedKey: String?) { 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 @@ -96,7 +100,7 @@ sealed class WCallResponse { }) } @Serializable class CallCapabilities(val encryption: Boolean) -@Serializable class ConnectionInfo(val localCandidate: RTCIceCandidate?, val remoteCandidate: RTCIceCandidate) { +@Serializable class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) { val text: String @Composable get() = when { localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host -> stringResource(R.string.call_connection_peer_to_peer) 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 ef1c52209b..2a9825bd1c 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 @@ -126,15 +126,7 @@ fun ChatView(chatModel: ChatModel) { if (invitation == null) { AlertManager.shared.showAlertMsg("Call already ended!") } else { - chatModel.activeCallInvitation.value = null - chatModel.activeCall.value = Call( - contact = contact, - callState = CallState.InvitationReceived, - localMedia = invitation.peerMedia, - sharedKey = invitation.sharedKey - ) - chatModel.showCallView.value = true - chatModel.callCommand.value = WCallCommand.Start(media = invitation.peerMedia, aesKey = invitation.sharedKey) + chatModel.callManager.acceptIncomingCall(invitation = invitation) } } ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallViewItem.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt similarity index 96% rename from apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallViewItem.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt index a2c0e166c0..65e85e0b75 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallViewItem.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt @@ -15,8 +15,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.SimpleButton +import chat.simplex.app.ui.theme.* @Composable fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, duration: Int, acceptCall: (Contact) -> Unit) { @@ -33,10 +32,10 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat AcceptCallButton(cInfo, acceptCall) } CICallStatus.Missed -> Icon(Icons.Outlined.Call, stringResource(R.string.icon_descr_call_missed), tint = Color.Red) - CICallStatus.Rejected -> Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_rejected), tint = HighOrLowlight) + CICallStatus.Rejected -> Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_rejected), tint = Color.Red) CICallStatus.Accepted -> ConnectingCallIcon() CICallStatus.Negotiated -> ConnectingCallIcon() - CICallStatus.Progress -> Icon(Icons.Filled.PhoneInTalk, stringResource(R.string.icon_descr_call_progress), tint = Color.Green) + CICallStatus.Progress -> Icon(Icons.Filled.PhoneInTalk, stringResource(R.string.icon_descr_call_progress), tint = SimplexGreen) CICallStatus.Ended -> Row { Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_ended), tint = HighOrLowlight, modifier = Modifier.padding(end = 4.dp)) Text(status.duration(duration), color = HighOrLowlight) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt index b19f17a2be..58310a2856 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt @@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.SupervisedUserCircle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale @@ -34,14 +35,15 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp) { fun ProfileImage( size: Dp, image: String? = null, - icon: ImageVector = Icons.Filled.AccountCircle + icon: ImageVector = Icons.Filled.AccountCircle, + color: Color = MaterialTheme.colors.secondary ) { Box(Modifier.size(size)) { if (image == null) { Icon( icon, contentDescription = stringResource(R.string.icon_descr_profile_image_placeholder), - tint = MaterialTheme.colors.secondary, + tint = color, modifier = Modifier.fillMaxSize() ) } else { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 478bc7b5fb..2235a82cbc 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -34,8 +34,10 @@ import java.util.* import kotlin.math.log2 import kotlin.math.pow -fun withApi(action: suspend CoroutineScope.() -> Unit): Job = - GlobalScope.launch { withContext(Dispatchers.Main, action) } +fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalScope, action) + +fun withScope(scope: CoroutineScope, action: suspend CoroutineScope.() -> Unit): Job = + scope.launch { withContext(Dispatchers.Main, action) } enum class KeyboardState { Opened, Closed diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt index 3825954bbc..8c3f467aac 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt @@ -40,13 +40,7 @@ fun SimpleXInfoLayout( showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), ) { Column(Modifier.fillMaxHeight(), horizontalAlignment = Alignment.Start) { - Image( - painter = painterResource(R.drawable.logo), - contentDescription = stringResource(R.string.image_descr_simplex_logo), - modifier = Modifier - .padding(vertical = 20.dp) - .fillMaxWidth(0.80f) - ) + SimpleXLogo() Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 16.dp)) @@ -79,6 +73,17 @@ fun SimpleXInfoLayout( } } +@Composable +fun SimpleXLogo() { + Image( + painter = painterResource(R.drawable.logo), + contentDescription = stringResource(R.string.image_descr_simplex_logo), + modifier = Modifier + .padding(vertical = 20.dp) + .fillMaxWidth(0.80f) + ) +} + @Composable private fun InfoRow(emoji: String, @StringRes titleId: Int, @StringRes textId: Int) { Row(Modifier.padding(bottom = 20.dp), verticalAlignment = Alignment.Top) { 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 9ab041d283..6431988fa4 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 @@ -10,6 +10,7 @@ import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -19,8 +20,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.app.BuildConfig import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.Profile +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.TerminalView @@ -84,16 +84,7 @@ fun SettingsLayout( Spacer(Modifier.height(30.dp)) SettingsSectionView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp) { - ProfileImage(size = 60.dp, profile.image) - Spacer(Modifier.padding(horizontal = 4.dp)) - Column { - Text( - profile.displayName, - style = MaterialTheme.typography.caption, - fontWeight = FontWeight.Bold, - ) - Text(profile.fullName) - } + ProfilePreview(profile) } Divider(Modifier.padding(horizontal = 8.dp)) SettingsSectionView(showModal { UserAddressView(it) }) { @@ -215,6 +206,19 @@ fun SettingsLayout( } } +@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary) { + ProfileImage(size = size, image = profileOf.image, color = color) + Spacer(Modifier.padding(horizontal = 4.dp)) + Column { + Text( + profileOf.displayName, + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.Bold, + ) + Text(profileOf.fullName) + } +} + @Composable fun SettingsSectionView(click: (() -> Unit)? = null, height: Dp = 46.dp, content: (@Composable () -> Unit)) { val modifier = Modifier diff --git a/apps/android/app/src/main/res/drawable-hdpi/icon.png b/apps/android/app/src/main/res/drawable-hdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9c6efe05895c97dff5a1dac719d9296a3f432d44 GIT binary patch literal 8996 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 literal 0 HcmV?d00001 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 624094c39d..5b665e6a1b 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -294,18 +294,20 @@ пропущенный звонок отклоненный звонок принятый звонок - соединяется… + звонок соединяется… активный звонок звонок завершён %1$s - ошибка соединения + ошибка звонка инициализация… ожидается ответ… ожидается подтверждение… получен ответ… + получено подтверждение… соединяется… соединено + завершен Новое поколение приватных сообщений @@ -348,7 +350,9 @@ e2e зашифрованный видеозвонок аудиозвонок (не e2e зашифрованный) e2e зашифрованный аудиозвонок - Принять + Принять + Отклонить + Закрыть Звонок уже завершен! видеозвонок аудиозвонок diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 3996718c6a..0c96aff28f 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -290,23 +290,25 @@ colored secret - + calling… - missed - rejected - accepted - connecting… - in progress - ended %1$s - error + missed call + rejected call + accepted call + connecting call… + call in progress + call ended %1$s + call error starting… waiting for answer… waiting for confirmation… received answer… + received confirmation… connecting… connected + ended The next generation of private messaging @@ -349,7 +351,9 @@ e2e encrypted video call audio call (not e2e encrypted) e2e encrypted audio call - Answer + Accept + Reject + Ignore Call already ended! video call audio call diff --git a/apps/ios/Shared/Views/Call/CallManager.swift b/apps/ios/Shared/Views/Call/CallManager.swift index f22535e9ad..19d049e80d 100644 --- a/apps/ios/Shared/Views/Call/CallManager.swift +++ b/apps/ios/Shared/Views/Call/CallManager.swift @@ -61,12 +61,12 @@ class CallManager { func endCall(call: Call, completed: @escaping () -> Void) { let m = ChatModel.shared if case .ended = call.callState { - logger.debug("CallController.provider CXEndCallAction: call ended") + logger.debug("CallManager.endCall: call ended") m.activeCall = nil m.showCallView = false completed() } else { - logger.debug("CallController.provider CXEndCallAction: ending call...") + logger.debug("CallManager.endCall: ending call...") m.callCommand = .end m.showCallView = false Task { diff --git a/apps/ios/Shared/Views/Call/WebRTCView.swift b/apps/ios/Shared/Views/Call/WebRTCView.swift index 428ef130b5..93a61c3f4d 100644 --- a/apps/ios/Shared/Views/Call/WebRTCView.swift +++ b/apps/ios/Shared/Views/Call/WebRTCView.swift @@ -111,78 +111,72 @@ func sendCallCommand(_ webView: WKWebView, _ command: WCallCommand) { webView.evaluateJavaScript(js) } -struct CallViewDebug: View { - @State private var commandStr = "" - @State private var rtcWebView: WKWebView? = nil - @State private var webViewMsg: WVAPIMessage? = nil - @FocusState private var keyboardVisible: Bool - - var body: some View { - VStack(spacing: 30) { - WebRTCView(rtcWebView: $rtcWebView, webViewMsg: $webViewMsg).frame(maxHeight: 260) - .onChange(of: webViewMsg) { _ in - if let resp = webViewMsg { - commandStr = encodeJSON(resp) - } - } - TextEditor(text: $commandStr) - .focused($keyboardVisible) - .disableAutocorrection(true) - .textInputAutocapitalization(.never) - .padding(.horizontal, 5) - .padding(.top, 2) - .frame(height: 112) - .overlay( - RoundedRectangle(cornerRadius: 10) - .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) - ) - HStack(spacing: 20) { - Button("Copy") { - UIPasteboard.general.string = commandStr - } - Button("Paste") { - commandStr = UIPasteboard.general.string ?? "" - } - Button("Clear") { - commandStr = "" - } - Button("Send") { - if let wv = rtcWebView, - let command: WCallCommand = decodeJSON(commandStr) { - sendCallCommand(wv, command) - } - } - } - HStack(spacing: 20) { - Button("Capabilities") { - if let wv = rtcWebView { - sendCallCommand(wv, .capabilities(useWorker: true)) - } - } - Button("Start") { - if let wv = rtcWebView { - sendCallCommand(wv, .start(media: .video)) - } - } - Button("Accept") { - - } - Button("Answer") { - - } - Button("ICE") { - - } - Button("End") { - - } - } - } - } -} +//struct CallViewDebug: View { +// @State private var commandStr = "" +// @State private var rtcWebView: WKWebView? = nil +// @State private var webViewMsg: WVAPIMessage? = nil +// @FocusState private var keyboardVisible: Bool // -//struct CallViewDebug_Previews: PreviewProvider { -// static var previews: some View { -// CallViewDebug() +// var body: some View { +// VStack(spacing: 30) { +// WebRTCView(rtcWebView: $rtcWebView, webViewMsg: $webViewMsg).frame(maxHeight: 260) +// .onChange(of: webViewMsg) { _ in +// if let resp = webViewMsg { +// commandStr = encodeJSON(resp) +// } +// } +// TextEditor(text: $commandStr) +// .focused($keyboardVisible) +// .disableAutocorrection(true) +// .textInputAutocapitalization(.never) +// .padding(.horizontal, 5) +// .padding(.top, 2) +// .frame(height: 112) +// .overlay( +// RoundedRectangle(cornerRadius: 10) +// .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) +// ) +// HStack(spacing: 20) { +// Button("Copy") { +// UIPasteboard.general.string = commandStr +// } +// Button("Paste") { +// commandStr = UIPasteboard.general.string ?? "" +// } +// Button("Clear") { +// commandStr = "" +// } +// Button("Send") { +// if let wv = rtcWebView, +// let command: WCallCommand = decodeJSON(commandStr) { +// sendCallCommand(wv, command) +// } +// } +// } +// HStack(spacing: 20) { +// Button("Capabilities") { +// if let wv = rtcWebView { +// sendCallCommand(wv, .capabilities(useWorker: true)) +// } +// } +// Button("Start") { +// if let wv = rtcWebView { +// sendCallCommand(wv, .start(media: .video)) +// } +// } +// Button("Accept") { +// +// } +// Button("Answer") { +// +// } +// Button("ICE") { +// +// } +// Button("End") { +// +// } +// } +// } // } //}