package org.thoughtcrime.securesms.calls import android.Manifest import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.media.AudioManager import android.os.Build import android.os.Bundle import android.view.* import androidx.activity.viewModels import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.bumptech.glide.load.engine.DiskCacheStrategy import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ActivityWebrtcBinding import org.apache.commons.lang3.time.DurationFormatUtils import org.session.libsession.avatars.ProfileContactPhoto import org.session.libsession.messaging.contacts.Contact import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.service.WebRtcCallService import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator import org.thoughtcrime.securesms.webrtc.AudioManagerCommand import org.thoughtcrime.securesms.webrtc.CallViewModel import org.thoughtcrime.securesms.webrtc.CallViewModel.State.* import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.* import java.util.* @AndroidEntryPoint class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { companion object { const val ACTION_PRE_OFFER = "pre-offer" const val ACTION_FULL_SCREEN_INTENT = "fullscreen-intent" const val ACTION_ANSWER = "answer" const val ACTION_END = "end-call" const val BUSY_SIGNAL_DELAY_FINISH = 5500L private const val CALL_DURATION_FORMAT = "HH:mm:ss" } private val viewModel by viewModels() private val glide by lazy { GlideApp.with(this) } private lateinit var binding: ActivityWebrtcBinding private var uiJob: Job? = null private var wantsToAnswer = false set(value) { field = value WebRtcCallService.broadcastWantsToAnswer(this, value) } private var hangupReceiver: BroadcastReceiver? = null override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { finish() return true } return super.onOptionsItemSelected(item) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) if (intent?.action == ACTION_ANSWER) { val answerIntent = WebRtcCallService.acceptCallIntent(this) ContextCompat.startForegroundService(this, answerIntent) } } override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) binding = ActivityWebrtcBinding.inflate(layoutInflater) setContentView(binding.root) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true) setTurnScreenOn(true) } window.addFlags( WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON ) volumeControlStream = AudioManager.STREAM_VOICE_CALL if (intent.action == ACTION_ANSWER) { answerCall() } if (intent.action == ACTION_PRE_OFFER) { wantsToAnswer = true answerCall() // this will do nothing, except update notification state } if (intent.action == ACTION_FULL_SCREEN_INTENT) { supportActionBar?.setDisplayHomeAsUpEnabled(false) } binding.microphoneButton.setOnClickListener { val audioEnabledIntent = WebRtcCallService.microphoneIntent(this, !viewModel.microphoneEnabled) startService(audioEnabledIntent) } binding.speakerPhoneButton.setOnClickListener { val command = AudioManagerCommand.SetUserDevice(if (viewModel.isSpeaker) EARPIECE else SPEAKER_PHONE) WebRtcCallService.sendAudioManagerCommand(this, command) } binding.acceptCallButton.setOnClickListener { if (viewModel.currentCallState == CALL_PRE_INIT) { wantsToAnswer = true updateControls() } answerCall() } binding.declineCallButton.setOnClickListener { val declineIntent = WebRtcCallService.denyCallIntent(this) startService(declineIntent) } hangupReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { finish() } } LocalBroadcastManager.getInstance(this) .registerReceiver(hangupReceiver!!, IntentFilter(ACTION_END)) binding.enableCameraButton.setOnClickListener { Permissions.with(this) .request(Manifest.permission.CAMERA) .onAllGranted { val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoEnabled) startService(intent) } .execute() } binding.switchCameraButton.setOnClickListener { startService(WebRtcCallService.flipCamera(this)) } binding.endCallButton.setOnClickListener { startService(WebRtcCallService.hangupIntent(this)) } } override fun onDestroy() { super.onDestroy() hangupReceiver?.let { receiver -> LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } } private fun answerCall() { val answerIntent = WebRtcCallService.acceptCallIntent(this) ContextCompat.startForegroundService(this, answerIntent) } private fun updateControls(state: CallViewModel.State? = null) { with (binding) { if (state == null) { if (wantsToAnswer) { controlGroup.isVisible = true remoteLoadingView.isVisible = true incomingControlGroup.isVisible = false } } else { controlGroup.isVisible = state in listOf( CALL_CONNECTED, CALL_OUTGOING, CALL_INCOMING ) || (state == CALL_PRE_INIT && wantsToAnswer) remoteLoadingView.isVisible = state !in listOf(CALL_CONNECTED, CALL_RINGING, CALL_PRE_INIT) || wantsToAnswer incomingControlGroup.isVisible = state in listOf(CALL_RINGING, CALL_PRE_INIT) && !wantsToAnswer } } } override fun onStart() { super.onStart() uiJob = lifecycleScope.launch { launch { viewModel.audioDeviceState.collect { state -> val speakerEnabled = state.selectedDevice == SPEAKER_PHONE // change drawable background to enabled or not binding.speakerPhoneButton.isSelected = speakerEnabled } } launch { viewModel.callState.collect { state -> Log.d("Loki", "Consuming view model state $state") when (state) { CALL_RINGING -> { if (wantsToAnswer) { answerCall() wantsToAnswer = false } } CALL_OUTGOING -> { } CALL_CONNECTED -> { wantsToAnswer = false } } updateControls(state) } } launch { viewModel.recipient.collect { latestRecipient -> if (latestRecipient.recipient != null) { val publicKey = latestRecipient.recipient.address.serialize() val displayName = getUserDisplayName(publicKey) supportActionBar?.title = displayName val signalProfilePicture = latestRecipient.recipient.contactPhoto val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject val sizeInPX = resources.getDimensionPixelSize(R.dimen.extra_large_profile_picture_size) if (signalProfilePicture != null && avatar != "0" && avatar != "") { glide.clear(binding.remoteRecipient) glide.load(signalProfilePicture) .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .circleCrop() .error( AvatarPlaceholderGenerator.generate( this@WebRtcCallActivity, sizeInPX, publicKey, displayName ) ) .into(binding.remoteRecipient) } else { glide.clear(binding.remoteRecipient) glide.load( AvatarPlaceholderGenerator.generate( this@WebRtcCallActivity, sizeInPX, publicKey, displayName ) ) .diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop() .into(binding.remoteRecipient) } } else { glide.clear(binding.remoteRecipient) } } } launch { while (isActive) { val startTime = viewModel.callStartTime if (startTime == -1L) { binding.callTime.isVisible = false } else { binding.callTime.isVisible = true binding.callTime.text = DurationFormatUtils.formatDuration( System.currentTimeMillis() - startTime, CALL_DURATION_FORMAT ) } delay(1_000) } } launch { viewModel.localAudioEnabledState.collect { isEnabled -> // change drawable background to enabled or not binding.microphoneButton.isSelected = !isEnabled } } launch { viewModel.localVideoEnabledState.collect { isEnabled -> binding.localRenderer.removeAllViews() if (isEnabled) { viewModel.localRenderer?.let { surfaceView -> surfaceView.setZOrderOnTop(true) binding.localRenderer.addView(surfaceView) } } binding.localRenderer.isVisible = isEnabled binding.enableCameraButton.isSelected = isEnabled } } launch { viewModel.cameraRotations.collect { (localRotation, remoteRotation) -> val screenRotation = getDeviceRotation() Log.d("Loki", "local rotation: $localRotation, remote rotation: $remoteRotation, screen rotation: $screenRotation") } } launch { viewModel.remoteVideoEnabledState.collect { isEnabled -> binding.remoteRenderer.removeAllViews() if (isEnabled) { viewModel.remoteRenderer?.let { surfaceView -> binding.remoteRenderer.addView(surfaceView) } } binding.remoteRenderer.isVisible = isEnabled binding.remoteRecipient.isVisible = !isEnabled } } } } private fun getDeviceRotation(): Int { val rotation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { display?.rotation } else { windowManager.defaultDisplay.rotation } return when (rotation) { Surface.ROTATION_0 -> 0 Surface.ROTATION_180 -> 180 Surface.ROTATION_270 -> 270 Surface.ROTATION_90 -> 90 else -> throw NullPointerException("Unrecognized rotation $rotation") } } private fun getUserDisplayName(publicKey: String): String { val contact = DatabaseComponent.get(this).sessionContactDatabase().getContactWithSessionID(publicKey) return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey } override fun onStop() { super.onStop() uiJob?.cancel() binding.remoteRenderer.removeAllViews() binding.localRenderer.removeAllViews() } }