diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/DefaultCallSignalingService.kt index 37ce1e62d6..a3bbcd6444 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/DefaultCallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/DefaultCallSignalingService.kt @@ -38,6 +38,7 @@ import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.RoomEventSender import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -100,8 +101,8 @@ internal class DefaultCallSignalingService @Inject constructor( override fun removeCallListener(listener: CallsListener) { callListeners.remove(listener) } - override fun getCallWithId(callId: String): MxCall? { + Timber.v("## VOIP getCallWithId $callId all calls ${activeCalls.map { it.callId }}") return activeCalls.find { it.callId == callId } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/model/MxCallImpl.kt index 53c075579a..90b475d5b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/model/MxCallImpl.kt @@ -83,6 +83,7 @@ internal class MxCallImpl( override fun offerSdp(sdp: SessionDescription) { if (!isOutgoing) return + Timber.v("## VOIP offerSdp $callId") state = CallState.DIALING CallInviteContent( callId = callId, @@ -113,6 +114,7 @@ internal class MxCallImpl( } override fun hangUp() { + Timber.v("## VOIP hangup $callId") CallHangupContent( callId = callId ) @@ -122,6 +124,7 @@ internal class MxCallImpl( } override fun accept(sdp: SessionDescription) { + Timber.v("## VOIP accept $callId") if (isOutgoing) return state = CallState.ANSWERING CallAnswerContent( diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index bab4f6c622..a992b11c06 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -365,5 +365,6 @@ Accept Decline + Hang Up diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 6b0253c5fc..64299ff1ae 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/FragmentManager.kt b/vector/src/main/java/im/vector/riotx/core/extensions/FragmentManager.kt index caf1bf90f8..83ac540830 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/FragmentManager.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/FragmentManager.kt @@ -17,9 +17,14 @@ package im.vector.riotx.core.extensions import androidx.fragment.app.FragmentTransaction +import im.vector.matrix.android.api.extensions.tryThis inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) { - beginTransaction().func().commitNow() + // Could throw and make the app crash + // e.g sharedActionViewModel.observe() + tryThis("Failed to commitTransactionNow") { + beginTransaction().func().commitNow() + } } inline fun androidx.fragment.app.FragmentManager.commitTransaction(func: FragmentTransaction.() -> FragmentTransaction) { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 270e67cf34..ba9e7320d2 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -165,6 +165,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } override fun onCreate(savedInstanceState: Bundle?) { + Timber.i("onCreate Activity ${this.javaClass.simpleName}") val vectorComponent = getVectorComponent() screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this) val timeForInjection = measureTimeMillis { @@ -252,6 +253,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { override fun onDestroy() { super.onDestroy() + Timber.i("onDestroy Activity ${this.javaClass.simpleName}") unBinder?.unbind() unBinder = null @@ -279,6 +281,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { override fun onPause() { super.onPause() + Timber.i("onPause Activity ${this.javaClass.simpleName}") rageShake.stop() diff --git a/vector/src/main/java/im/vector/riotx/core/services/CallRingPlayer.kt b/vector/src/main/java/im/vector/riotx/core/services/CallRingPlayer.kt new file mode 100644 index 0000000000..f7f64d65f5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/services/CallRingPlayer.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.services + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.MediaPlayer +import android.os.Build +import im.vector.riotx.R +import timber.log.Timber + +class CallRingPlayer( + context: Context +) { + + private val applicationContext = context.applicationContext + + private var player: MediaPlayer? = null + + fun start() { + val audioManager: AudioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + player?.release() + player = createPlayer() + + // Check if sound is enabled + val ringerMode = audioManager.ringerMode + if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) { + try { + if (player?.isPlaying == false) { + player?.start() + Timber.v("## VOIP Starting ringing") + } else { + Timber.v("## VOIP already playing") + } + } catch (failure: Throwable) { + Timber.e(failure, "## VOIP Failed to start ringing") + player = null + } + } else { + Timber.v("## VOIP Can't play $player ode $ringerMode") + } + } + + fun stop() { + player?.release() + player = null + } + + private fun createPlayer(): MediaPlayer? { + try { + val mediaPlayer = MediaPlayer.create(applicationContext, R.raw.ring) + + mediaPlayer.setOnErrorListener(MediaPlayerErrorListener()) + mediaPlayer.isLooping = true + if (Build.VERSION.SDK_INT <= 21) { + @Suppress("DEPRECATION") + mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING) + } else { + mediaPlayer.setAudioAttributes(AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) + .build()) + } + return mediaPlayer + } catch (failure: Throwable) { + Timber.e(failure, "Failed to create Call ring player") + return null + } + } + + inner class MediaPlayerErrorListener : MediaPlayer.OnErrorListener { + override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean { + Timber.w("onError($mp, $what, $extra") + player = null + return false + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt index 8337e56403..f10bbd908d 100644 --- a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt @@ -1,6 +1,6 @@ - /* * Copyright 2019 New Vector Ltd + * Copyright 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ package im.vector.riotx.core.services import android.content.Context import android.content.Intent import android.os.Binder -import android.text.TextUtils import androidx.core.content.ContextCompat import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.features.call.WebRtcPeerConnectionManager @@ -38,7 +37,7 @@ class CallService : VectorService() { /** * call in progress (foreground notification) */ - private var mCallIdInProgress: String? = null +// private var mCallIdInProgress: String? = null private lateinit var notificationUtils: NotificationUtils private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager @@ -46,15 +45,19 @@ class CallService : VectorService() { /** * incoming (foreground notification) */ - private var mIncomingCallId: String? = null +// private var mIncomingCallId: String? = null + + private var callRingPlayer: CallRingPlayer? = null override fun onCreate() { super.onCreate() notificationUtils = vectorComponent().notificationUtils() webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager() + callRingPlayer = CallRingPlayer(applicationContext) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Timber.v("## VOIP onStartCommand $intent") if (intent == null) { // Service started again by the system. // TODO What do we do here? @@ -62,12 +65,34 @@ class CallService : VectorService() { } when (intent.action) { - ACTION_INCOMING_CALL -> displayIncomingCallNotification(intent) - ACTION_PENDING_CALL -> displayCallInProgressNotification(intent) - ACTION_NO_ACTIVE_CALL -> hideCallNotifications() - else -> + ACTION_INCOMING_RINGING_CALL -> { + callRingPlayer?.start() + displayIncomingCallNotification(intent) + } + ACTION_OUTGOING_RINGING_CALL -> { + callRingPlayer?.start() + displayOutgoingRingingCallNotification(intent) + } + ACTION_ONGOING_CALL -> { + callRingPlayer?.stop() + displayCallInProgressNotification(intent) + } + ACTION_NO_ACTIVE_CALL -> hideCallNotifications() + ACTION_CALL_CONNECTING -> { + // lower notification priority + displayCallInProgressNotification(intent) + // stop ringing + callRingPlayer?.stop() + } + ACTION_ONGOING_CALL_BG -> { + // there is an ongoing call but call activity is in background + displayCallOnGoingInBackground(intent) + } + else -> { // Should not happen + callRingPlayer?.stop() myStopSelf() + } } // We want the system to restore the service if killed @@ -87,29 +112,29 @@ class CallService : VectorService() { * @param callId the callId */ private fun displayIncomingCallNotification(intent: Intent) { - Timber.v("displayIncomingCallNotification") + Timber.v("## VOIP displayIncomingCallNotification $intent") // the incoming call in progress is already displayed - if (!TextUtils.isEmpty(mIncomingCallId)) { - Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed") - } else if (!TextUtils.isEmpty(mCallIdInProgress)) { - Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed") - } else -// if (null == webRtcPeerConnectionManager.currentCall) - { - val callId = intent.getStringExtra(EXTRA_CALL_ID) +// if (!TextUtils.isEmpty(mIncomingCallId)) { +// Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed") +// } else if (!TextUtils.isEmpty(mCallIdInProgress)) { +// Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed") +// } else +// // if (null == webRtcPeerConnectionManager.currentCall) +// { + val callId = intent.getStringExtra(EXTRA_CALL_ID) - Timber.v("displayIncomingCallNotification : display the dedicated notification") - val notification = notificationUtils.buildIncomingCallNotification( - intent.getBooleanExtra(EXTRA_IS_VIDEO, false), - intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", - intent.getStringExtra(EXTRA_MATRIX_ID) ?: "", - callId ?: "") - startForeground(NOTIFICATION_ID, notification) + Timber.v("displayIncomingCallNotification : display the dedicated notification") + val notification = notificationUtils.buildIncomingCallNotification( + intent.getBooleanExtra(EXTRA_IS_VIDEO, false), + intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", + intent.getStringExtra(EXTRA_ROOM_ID) ?: "", + callId ?: "") + startForeground(NOTIFICATION_ID, notification) - mIncomingCallId = callId +// mIncomingCallId = callId - // turn the screen on for 3 seconds + // turn the screen on for 3 seconds // if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) { // try { // val pm = getSystemService(Context.POWER_SERVICE) as PowerManager @@ -123,16 +148,29 @@ class CallService : VectorService() { // } // // } - } +// } // else { // Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call") // } } + private fun displayOutgoingRingingCallNotification(intent: Intent) { + val callId = intent.getStringExtra(EXTRA_CALL_ID) + + Timber.v("displayOutgoingCallNotification : display the dedicated notification") + val notification = notificationUtils.buildOutgoingRingingCallNotification( + intent.getBooleanExtra(EXTRA_IS_VIDEO, false), + intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", + intent.getStringExtra(EXTRA_ROOM_ID) ?: "", + callId ?: "") + startForeground(NOTIFICATION_ID, notification) + } + /** * Display a call in progress notification. */ private fun displayCallInProgressNotification(intent: Intent) { + Timber.v("## VOIP displayCallInProgressNotification") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" val notification = notificationUtils.buildPendingCallNotification( @@ -144,7 +182,27 @@ class CallService : VectorService() { startForeground(NOTIFICATION_ID, notification) - mCallIdInProgress = callId + // mCallIdInProgress = callId + } + + /** + * Display a call in progress notification. + */ + private fun displayCallOnGoingInBackground(intent: Intent) { + Timber.v("## VOIP displayCallInProgressNotification") + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" + + val notification = notificationUtils.buildPendingCallNotification( + isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false), + roomName = intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", + roomId = intent.getStringExtra(EXTRA_ROOM_ID) ?: "", + matrixId = intent.getStringExtra(EXTRA_MATRIX_ID) ?: "", + callId = callId, + fromBg = true) + + startForeground(NOTIFICATION_ID, notification) + + // mCallIdInProgress = callId } /** @@ -159,6 +217,11 @@ class CallService : VectorService() { myStopSelf() } + override fun onDestroy() { + super.onDestroy() + callRingPlayer?.stop() + } + fun addConnection(callConnection: CallConnection) { connections[callConnection.callId] = callConnection } @@ -166,10 +229,14 @@ class CallService : VectorService() { companion object { private const val NOTIFICATION_ID = 6480 - private const val ACTION_INCOMING_CALL = "im.vector.riotx.core.services.CallService.INCOMING_CALL" - private const val ACTION_PENDING_CALL = "im.vector.riotx.core.services.CallService.PENDING_CALL" + private const val ACTION_INCOMING_RINGING_CALL = "im.vector.riotx.core.services.CallService.ACTION_INCOMING_RINGING_CALL" + private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.riotx.core.services.CallService.ACTION_OUTGOING_RINGING_CALL" + private const val ACTION_CALL_CONNECTING = "im.vector.riotx.core.services.CallService.ACTION_CALL_CONNECTING" + private const val ACTION_ONGOING_CALL = "im.vector.riotx.core.services.CallService.ACTION_ONGOING_CALL" + private const val ACTION_ONGOING_CALL_BG = "im.vector.riotx.core.services.CallService.ACTION_ONGOING_CALL_BG" private const val ACTION_NO_ACTIVE_CALL = "im.vector.riotx.core.services.CallService.NO_ACTIVE_CALL" -// private const val ACTION_ON_ACTIVE_CALL = "im.vector.riotx.core.services.CallService.ACTIVE_CALL" +// private const val ACTION_ACTIVITY_VISIBLE = "im.vector.riotx.core.services.CallService.ACTION_ACTIVITY_VISIBLE" +// private const val ACTION_STOP_RINGING = "im.vector.riotx.core.services.CallService.ACTION_STOP_RINGING" private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO" private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME" @@ -177,15 +244,15 @@ class CallService : VectorService() { private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID" private const val EXTRA_CALL_ID = "EXTRA_CALL_ID" - fun onIncomingCall(context: Context, - isVideo: Boolean, - roomName: String, - roomId: String, - matrixId: String, - callId: String) { + fun onIncomingCallRinging(context: Context, + isVideo: Boolean, + roomName: String, + roomId: String, + matrixId: String, + callId: String) { val intent = Intent(context, CallService::class.java) .apply { - action = ACTION_INCOMING_CALL + action = ACTION_INCOMING_RINGING_CALL putExtra(EXTRA_IS_VIDEO, isVideo) putExtra(EXTRA_ROOM_NAME, roomName) putExtra(EXTRA_ROOM_ID, roomId) @@ -196,24 +263,43 @@ class CallService : VectorService() { ContextCompat.startForegroundService(context, intent) } -// fun onActiveCall(context: Context, -// isVideo: Boolean, -// roomName: String, -// roomId: String, -// matrixId: String, -// callId: String) { -// val intent = Intent(context, CallService::class.java) -// .apply { -// action = ACTION_ON_ACTIVE_CALL -// putExtra(EXTRA_IS_VIDEO, isVideo) -// putExtra(EXTRA_ROOM_NAME, roomName) -// putExtra(EXTRA_ROOM_ID, roomId) -// putExtra(EXTRA_MATRIX_ID, matrixId) -// putExtra(EXTRA_CALL_ID, callId) -// } -// -// ContextCompat.startForegroundService(context, intent) -// } + fun onOnGoingCallBackground(context: Context, + isVideo: Boolean, + roomName: String, + roomId: String, + matrixId: String, + callId: String) { + val intent = Intent(context, CallService::class.java) + .apply { + action = ACTION_ONGOING_CALL_BG + putExtra(EXTRA_IS_VIDEO, isVideo) + putExtra(EXTRA_ROOM_NAME, roomName) + putExtra(EXTRA_ROOM_ID, roomId) + putExtra(EXTRA_MATRIX_ID, matrixId) + putExtra(EXTRA_CALL_ID, callId) + } + + ContextCompat.startForegroundService(context, intent) + } + + fun onOutgoingCallRinging(context: Context, + isVideo: Boolean, + roomName: String, + roomId: String, + matrixId: String, + callId: String) { + val intent = Intent(context, CallService::class.java) + .apply { + action = ACTION_OUTGOING_RINGING_CALL + putExtra(EXTRA_IS_VIDEO, isVideo) + putExtra(EXTRA_ROOM_NAME, roomName) + putExtra(EXTRA_ROOM_ID, roomId) + putExtra(EXTRA_MATRIX_ID, matrixId) + putExtra(EXTRA_CALL_ID, callId) + } + + ContextCompat.startForegroundService(context, intent) + } fun onPendingCall(context: Context, isVideo: Boolean, @@ -223,7 +309,7 @@ class CallService : VectorService() { callId: String) { val intent = Intent(context, CallService::class.java) .apply { - action = ACTION_PENDING_CALL + action = ACTION_ONGOING_CALL putExtra(EXTRA_IS_VIDEO, isVideo) putExtra(EXTRA_ROOM_NAME, roomName) putExtra(EXTRA_ROOM_ID, roomId) diff --git a/vector/src/main/java/im/vector/riotx/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/riotx/features/call/VectorCallActivity.kt index 256592656b..a6aede7f48 100644 --- a/vector/src/main/java/im/vector/riotx/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/call/VectorCallActivity.kt @@ -20,6 +20,7 @@ package im.vector.riotx.features.call import android.app.KeyguardManager import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP import android.os.Build import android.os.Bundle import android.os.Parcelable @@ -31,6 +32,7 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.updatePadding import butterknife.BindView +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel import com.jakewharton.rxbinding3.view.clicks @@ -46,10 +48,11 @@ import im.vector.riotx.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.riotx.core.utils.allGranted import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.RoomDetailActivity +import im.vector.riotx.features.home.room.detail.RoomDetailArgs import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_call.* -import kotlinx.android.synthetic.main.fragment_attachments_preview.* import org.webrtc.EglBase import org.webrtc.RendererCommon import org.webrtc.SurfaceViewRenderer @@ -101,19 +104,6 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis override fun doBeforeSetContentView() { // Set window styles for fullscreen-window size. Needs to be done before adding content. requestWindowFeature(Window.FEATURE_NO_TITLE) - window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setTurnScreenOn(true) - setShowWhenLocked(true) - getSystemService(KeyguardManager::class.java)?.requestDismissKeyguard(this, null) - } else { - @Suppress("DEPRECATION") - window.addFlags( - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD - ) - } hideSystemUI() setContentView(R.layout.activity_call) @@ -179,31 +169,22 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis if (intent.hasExtra(MvRx.KEY_ARG)) { callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!! } else { + Timber.e("## VOIP missing callArgs for VectorCall Activity") + CallService.onNoActiveCall(this) finish() } + Timber.v("## VOIP EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}") + if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) { + turnScreenOnAndKeyguardOff() + } + constraintLayout.clicks() .throttleFirst(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { toggleUiSystemVisibility() } .disposeOnDestroy() - if (isFirstCreation()) { - // Reduce priority of notification as the activity is on screen - CallService.onPendingCall( - this, - callArgs.isVideoCall, - callArgs.participantUserId, - callArgs.roomId, - "", - callArgs.callId ?: "" - ) - } - - rootEglBase = EglUtils.rootEglBase ?: return Unit.also { - finish() - } - configureCallViews() callViewModel.subscribe(this) { @@ -229,8 +210,68 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis } } + override fun onDestroy() { + super.onDestroy() + peerConnectionManager.detachRenderers() + turnScreenOffAndKeyguardOn() + } + +// override fun onResume() { +// super.onResume() +// } +// +// override fun onStop() { +// super.onStop() +// when(callViewModel.call?.state) { +// CallState.DIALING -> { +// CallService.onIncomingCall( +// this, +// callArgs.isVideoCall, +// callArgs.participantUserId, +// callArgs.roomId, +// "", +// callArgs.callId ?: "" +// ) +// } +// CallState.LOCAL_RINGING -> { +// CallService.onIncomingCall( +// this, +// callArgs.isVideoCall, +// callArgs.participantUserId, +// callArgs.roomId, +// "", +// callArgs.callId ?: "" +// ) +// } +// CallState.ANSWERING, +// CallState.CONNECTING, +// CallState.CONNECTED -> { +// CallService.onPendingCall( +// this, +// callArgs.isVideoCall, +// callArgs.participantUserId, +// callArgs.roomId, +// "", +// callArgs.callId ?: "" +// ) +// } +// CallState.TERMINATED , +// CallState.IDLE , +// null -> { +// +// } +// } +// } + private fun renderState(state: VectorCallViewState) { Timber.v("## VOIP renderState call $state") + if (state.callState is Fail) { + // be sure to clear notification + CallService.onNoActiveCall(this) + finish() + return + } + callControlsView.updateForState(state) when (state.callState.invoke()) { CallState.IDLE, @@ -271,6 +312,8 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis configureCallInfo(state) callStatusText.text = null } + // ensure all attached? + peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer, null) } CallState.TERMINATED -> { finish() @@ -290,20 +333,6 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis private fun configureCallViews() { callControlsView.interactionListener = this -// if (callArgs.isVideoCall) { -// iv_call_speaker.isVisible = false -// iv_call_flip_camera.isVisible = true -// iv_call_videocam_off.isVisible = true -// } else { -// iv_call_speaker.isVisible = true -// iv_call_flip_camera.isVisible = false -// iv_call_videocam_off.isVisible = false -// } -// -// iv_end_call.setOnClickListener { -// callViewModel.handle(VectorCallViewActions.EndCall) -// finish() -// } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { @@ -315,7 +344,12 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis } } - private fun start(): Boolean { + private fun start() { + rootEglBase = EglUtils.rootEglBase ?: return Unit.also { + Timber.v("## VOIP rootEglBase is null") + finish() + } + // Init Picture in Picture renderer pipRenderer.init(rootEglBase!!.eglBaseContext, null) pipRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) @@ -330,16 +364,31 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer, intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }) - return false } - override fun onPause() { - peerConnectionManager.detachRenderers() - super.onPause() - } +// override fun onResume() { +// super.onResume() +// withState(callViewModel) { +// if(it.callState.invoke() == CallState.CONNECTED) { +// peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer) +// } +// } +// } +// override fun onPause() { +// peerConnectionManager.detachRenderers() +// super.onPause() +// } private fun handleViewEvents(event: VectorCallViewEvents?) { - Timber.v("handleViewEvents $event") + Timber.v("## VOIP handleViewEvents $event") + when (event) { + VectorCallViewEvents.DismissNoCall -> { + CallService.onNoActiveCall(this) + finish() + } + null -> { + } + } // when (event) { // is VectorCallViewEvents.CallAnswered -> { // } @@ -360,6 +409,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis fun newIntent(context: Context, mxCall: MxCallDetail): Intent { return Intent(context, VectorCallActivity::class.java).apply { + // what could be the best flags? flags = Intent.FLAG_ACTIVITY_NEW_TASK putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall, false)) putExtra(EXTRA_MODE, OUTGOING_CREATED) @@ -375,7 +425,8 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis accept: Boolean, mode: String?): Intent { return Intent(context, VectorCallActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK + // what could be the best flags? + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP putExtra(MvRx.KEY_ARG, CallArgs(roomId, callId, otherUserId, isIncomingCall, isVideoCall, accept)) putExtra(EXTRA_MODE, mode) } @@ -403,11 +454,48 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis } override fun returnToChat() { - // TODO, what if the room is not in backstack?? + val args = RoomDetailArgs(callArgs.roomId) + val intent = RoomDetailActivity.newIntent(this, args).apply { + flags = FLAG_ACTIVITY_CLEAR_TOP + } + startActivity(intent) + // is it needed? finish() } override fun didTapMore() { CallControlsBottomSheet().show(supportFragmentManager, "Controls") } + + // Needed to let you answer call when phone is locked + private fun turnScreenOnAndKeyguardOff() { + Timber.v("## VOIP turnScreenOnAndKeyguardOff") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true) + setTurnScreenOn(true) + } else { + window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + ) + } + + with(getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + requestDismissKeyguard(this@VectorCallActivity, null) + } + } + } + + private fun turnScreenOffAndKeyguardOn() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(false) + setTurnScreenOn(false) + } else { + window.clearFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + ) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/riotx/features/call/VectorCallViewModel.kt index 47888dfe76..b8af552dc4 100644 --- a/vector/src/main/java/im/vector/riotx/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/call/VectorCallViewModel.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.call import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory @@ -58,6 +59,7 @@ sealed class VectorCallViewActions : VectorViewModelAction { sealed class VectorCallViewEvents : VectorViewEvents { + object DismissNoCall : VectorCallViewEvents() // data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents() // data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents() // object CallAccepted : VectorCallViewEvents() @@ -117,6 +119,12 @@ class VectorCallViewModel @AssistedInject constructor( soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice() ) } + } ?: run { + setState { + copy( + callState = Fail(IllegalArgumentException("No call")) + ) + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt index ecbad9e269..0c7f2be691 100644 --- a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt @@ -266,6 +266,23 @@ class WebRtcPeerConnectionManager @Inject constructor( Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") this.localSurfaceRenderer = WeakReference(localViewRenderer) this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer) + + // The call is going to resume from background, we can reduce notif + currentCall?.mxCall + ?.takeIf { it.state == CallState.CONNECTING || it.state == CallState.CONNECTED } + ?.let { mxCall -> + val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() + ?: mxCall.roomId + // Start background service with notification + CallService.onPendingCall( + context = context, + isVideo = mxCall.isVideoCall, + roomName = name, + roomId = mxCall.roomId, + matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", + callId = mxCall.callId) + } + getTurnServer { turnServer -> val call = currentCall ?: return@getTurnServer when (mode) { @@ -314,6 +331,19 @@ class WebRtcPeerConnectionManager @Inject constructor( } private fun internalAcceptIncomingCall(callContext: CallContext, turnServerResponse: TurnServerResponse?) { + val mxCall = callContext.mxCall + // Update service state + + val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() + ?: mxCall.roomId + CallService.onPendingCall( + context = context, + isVideo = mxCall.isVideoCall, + roomName = name, + roomId = mxCall.roomId, + matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", + callId = mxCall.callId + ) executor.execute { // 1) create peer connection createPeerConnection(callContext, turnServerResponse) @@ -435,7 +465,8 @@ class WebRtcPeerConnectionManager @Inject constructor( fun acceptIncomingCall() { Timber.v("## VOIP acceptIncomingCall from state ${currentCall?.mxCall?.state}") - if (currentCall?.mxCall?.state == CallState.LOCAL_RINGING) { + val mxCall = currentCall?.mxCall + if (mxCall?.state == CallState.LOCAL_RINGING) { getTurnServer { turnServer -> internalAcceptIncomingCall(currentCall!!, turnServer) } @@ -443,6 +474,24 @@ class WebRtcPeerConnectionManager @Inject constructor( } fun detachRenderers() { + // The call is going to continue in background, so ensure notification is visible + currentCall?.mxCall + ?.takeIf { it.state == CallState.CONNECTING || it.state == CallState.CONNECTED } + ?.let { mxCall -> + // Start background service with notification + + val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() + ?: mxCall.otherUserId + CallService.onOnGoingCallBackground( + context = context, + isVideo = mxCall.isVideoCall, + roomName = name, + roomId = mxCall.roomId, + matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", + callId = mxCall.callId + ) + } + Timber.v("## VOIP detachRenderers") // currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) } localSurfaceRenderer?.get()?.let { @@ -496,6 +545,16 @@ class WebRtcPeerConnectionManager @Inject constructor( audioManager.startForCall(createdCall) currentCall = callContext + val name = sessionHolder.getSafeActiveSession()?.getUser(createdCall.otherUserId)?.getBestName() + ?: createdCall.otherUserId + CallService.onOutgoingCallRinging( + context = context, + isVideo = createdCall.isVideoCall, + roomName = name, + roomId = createdCall.roomId, + matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", + callId = createdCall.callId) + executor.execute { callContext.remoteCandidateSource = ReplaySubject.create() } @@ -524,9 +583,8 @@ class WebRtcPeerConnectionManager @Inject constructor( Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") // TODO What if a call is currently active? if (currentCall != null) { - Timber.w("## VOIP TODO: Automatically reject incoming call?") - mxCall.hangUp() - audioManager.stop() + Timber.w("## VOIP receiving incoming call while already in call?") + // Just ignore, maybe we could answer from other session? return } @@ -537,12 +595,17 @@ class WebRtcPeerConnectionManager @Inject constructor( callContext.remoteCandidateSource = ReplaySubject.create() } - CallService.onIncomingCall(context, - mxCall.isVideoCall, - mxCall.otherUserId, - mxCall.roomId, - sessionHolder.getSafeActiveSession()?.myUserId ?: "", - mxCall.callId) + // Start background service with notification + val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() + ?: mxCall.otherUserId + CallService.onIncomingCallRinging( + context = context, + isVideo = mxCall.isVideoCall, + roomName = name, + roomId = mxCall.roomId, + matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", + callId = mxCall.callId + ) callContext.offerSdp = callInviteContent.offer } @@ -575,12 +638,19 @@ class WebRtcPeerConnectionManager @Inject constructor( } fun endCall() { + // Update service state + CallService.onNoActiveCall(context) + // close tracks ASAP + currentCall?.localVideoTrack?.setEnabled(false) + currentCall?.localVideoTrack?.setEnabled(false) + currentCall?.cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } } + currentCall?.mxCall?.hangUp() currentCall = null audioManager.stop() @@ -592,6 +662,18 @@ class WebRtcPeerConnectionManager @Inject constructor( if (call.mxCall.callId != callAnswerContent.callId) return Unit.also { Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}") } + val mxCall = call.mxCall + // Update service state + val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() + ?: mxCall.otherUserId + CallService.onPendingCall( + context = context, + isVideo = mxCall.isVideoCall, + roomName = name, + roomId = mxCall.roomId, + matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", + callId = mxCall.callId + ) executor.execute { Timber.v("## VOIP onCallAnswerReceived ${callAnswerContent.callId}") val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp) @@ -626,7 +708,8 @@ class WebRtcPeerConnectionManager @Inject constructor( * One or more of the ICE transports on the connection is in the "failed" state. */ PeerConnection.PeerConnectionState.FAILED -> { - endCall() + // This can be temporary, e.g when other ice not yet received... + // callContext.mxCall.state = CallState.ERROR } /** * At least one of the connection's ICE transports (RTCIceTransports or RTCDtlsTransports) are in the "new" state, @@ -711,7 +794,9 @@ class WebRtcPeerConnectionManager @Inject constructor( * It is, however, possible that the ICE agent did find compatible connections for some components. */ PeerConnection.IceConnectionState.FAILED -> { - callContext.mxCall.hangUp() + // I should not hangup here.. + // because new candidates could arrive + // callContext.mxCall.hangUp() } /** * The ICE agent has finished gathering candidates, has checked all pairs against one another, and has found a connection for all components. @@ -742,7 +827,7 @@ class WebRtcPeerConnectionManager @Inject constructor( remoteVideoTrack.setEnabled(true) callContext.remoteVideoTrack = remoteVideoTrack // sink to renderer if attached - remoteSurfaceRenderer?.get().let { remoteVideoTrack.addSink(it) } + remoteSurfaceRenderer?.get()?.let { remoteVideoTrack.addSink(it) } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/call/service/CallHeadsUpActionReceiver.kt b/vector/src/main/java/im/vector/riotx/features/call/service/CallHeadsUpActionReceiver.kt index 6b38a272a2..199dcd3b14 100644 --- a/vector/src/main/java/im/vector/riotx/features/call/service/CallHeadsUpActionReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/call/service/CallHeadsUpActionReceiver.kt @@ -23,9 +23,15 @@ import im.vector.riotx.core.di.HasVectorInjector import im.vector.riotx.features.call.WebRtcPeerConnectionManager import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.settings.VectorLocale.context +import timber.log.Timber class CallHeadsUpActionReceiver : BroadcastReceiver() { + companion object { + const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY" + const val CALL_ACTION_REJECT = 0 + } + private lateinit var peerConnectionManager: WebRtcPeerConnectionManager private lateinit var notificationUtils: NotificationUtils @@ -38,10 +44,9 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() { } override fun onReceive(context: Context, intent: Intent?) { -// when (intent?.getIntExtra(CallHeadsUpService.EXTRA_CALL_ACTION_KEY, 0)) { -// CallHeadsUpService.CALL_ACTION_ANSWER -> onCallAnswerClicked(context) -// CallHeadsUpService.CALL_ACTION_REJECT -> onCallRejectClicked() -// } + when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) { + CALL_ACTION_REJECT -> onCallRejectClicked() + } // Not sure why this should be needed // val it = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) @@ -51,10 +56,10 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() { // context.stopService(Intent(context, CallHeadsUpService::class.java)) } -// private fun onCallRejectClicked() { -// Timber.d("onCallRejectClicked") -// peerConnectionManager.endCall() -// } + private fun onCallRejectClicked() { + Timber.d("onCallRejectClicked") + peerConnectionManager.endCall() + } // private fun onCallAnswerClicked(context: Context) { // Timber.d("onCallAnswerClicked") diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index 72471ccf1d..88a19fff89 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -283,31 +283,29 @@ class NotificationUtils @Inject constructor(private val context: Context, .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) .setLights(accentColor, 500, 500) + .setOngoing(true) // Compat: Display the incoming call notification on the lock screen builder.priority = NotificationCompat.PRIORITY_HIGH - // clear the activity stack to home activity - // val intent = Intent(context, HomeActivity::class.java) - // .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - // TODO .putExtra(VectorHomeActivity.EXTRA_CALL_SESSION_ID, matrixId) - // TODO .putExtra(VectorHomeActivity.EXTRA_CALL_ID, callId) - - // Recreate the back stack -// val stackBuilder = TaskStackBuilder.create(context) -// .addParentStack(HomeActivity::class.java) -// .addNextIntent(intent) - - // android 4.3 issue - // use a generator for the private requestCode. - // When using 0, the intent is not created/launched when the user taps on the notification. // val requestId = Random.nextInt(1000) // val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT) - val contentPendingIntent = TaskStackBuilder.create(context) - .addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) - .addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, otherUserId, true, isVideo, false, VectorCallActivity.INCOMING_RINGING)) - .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) + + val contentIntent = VectorCallActivity.newIntent( + context, callId, roomId, otherUserId, true, isVideo, + false, VectorCallActivity.INCOMING_RINGING + ).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + data = Uri.parse("foobar://$callId") + } + val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) + +// val contentPendingIntent = TaskStackBuilder.create(context) +// .addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) +// .addNextIntent(RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId = roomId)) +// .addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, otherUserId, true, isVideo, false, VectorCallActivity.INCOMING_RINGING)) +// .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) val answerCallPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) @@ -321,7 +319,12 @@ class NotificationUtils @Inject constructor(private val context: Context, // putExtra(CallHeadsUpService.EXTRA_CALL_ACTION_KEY, CallHeadsUpService.CALL_ACTION_REJECT) } // val answerCallPendingIntent = PendingIntent.getBroadcast(context, requestId, answerCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT) - val rejectCallPendingIntent = PendingIntent.getBroadcast(context, requestId + 1, rejectCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT) + val rejectCallPendingIntent = PendingIntent.getBroadcast( + context, + requestId + 1, + rejectCallActionReceiver, + PendingIntent.FLAG_UPDATE_CURRENT + ) builder.addAction( NotificationCompat.Action( @@ -340,6 +343,54 @@ class NotificationUtils @Inject constructor(private val context: Context, rejectCallPendingIntent) ) + builder.setFullScreenIntent(contentPendingIntent, true) + + return builder.build() + } + + fun buildOutgoingRingingCallNotification(isVideo: Boolean, + otherUserId: String, + roomId: String, + callId: String): Notification { + val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) + + val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) + .setContentTitle(ensureTitleNotEmpty(otherUserId)) + .apply { + setContentText(stringProvider.getString(R.string.call_ring)) + } + .setSmallIcon(R.drawable.incoming_call_notification_transparent) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setLights(accentColor, 500, 500) + .setOngoing(true) + + val requestId = Random.nextInt(1000) + + val contentIntent = VectorCallActivity.newIntent( + context, callId, roomId, otherUserId, true, isVideo, + false, null).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + data = Uri.parse("foobar://$callId") + } + val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) + + val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { + putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT) + } + + val rejectCallPendingIntent = PendingIntent.getBroadcast( + context, + requestId + 1, + rejectCallActionReceiver, + PendingIntent.FLAG_UPDATE_CURRENT + ) + + builder.addAction( + NotificationCompat.Action( + IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)), + context.getString(R.string.call_notification_hangup), + rejectCallPendingIntent) + ) builder.setContentIntent(contentPendingIntent) return builder.build() @@ -360,8 +411,8 @@ class NotificationUtils @Inject constructor(private val context: Context, roomName: String, roomId: String, matrixId: String, - callId: String): Notification { - val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) + callId: String, fromBg: Boolean = false): Notification { + val builder = NotificationCompat.Builder(context, if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(ensureTitleNotEmpty(roomName)) .apply { if (isVideo) { @@ -373,7 +424,29 @@ class NotificationUtils @Inject constructor(private val context: Context, .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) - builder.priority = NotificationCompat.PRIORITY_DEFAULT + if (fromBg) { + builder.priority = NotificationCompat.PRIORITY_LOW + builder.setOngoing(true) + } + + val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { + data = Uri.parse("mxcall://end?$callId") + putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT) + } + + val rejectCallPendingIntent = PendingIntent.getBroadcast( + context, + System.currentTimeMillis().toInt(), + rejectCallActionReceiver, + PendingIntent.FLAG_UPDATE_CURRENT + ) + + builder.addAction( + NotificationCompat.Action( + IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)), + context.getString(R.string.call_notification_hangup), + rejectCallPendingIntent) + ) val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) @@ -381,32 +454,8 @@ class NotificationUtils @Inject constructor(private val context: Context, .addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, false, null)) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) - // android 4.3 issue - // use a generator for the private requestCode. - // When using 0, the intent is not created/launched when the user taps on the notification. builder.setContentIntent(contentPendingIntent) - /* TODO - // Build the pending intent for when the notification is clicked - val roomIntent = Intent(context, VectorRoomActivity::class.java) - .putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId) - .putExtra(VectorRoomActivity.EXTRA_MATRIX_ID, matrixId) - .putExtra(VectorRoomActivity.EXTRA_START_CALL_ID, callId) - - // Recreate the back stack - val stackBuilder = TaskStackBuilder.create(context) - .addParentStack(VectorRoomActivity::class.java) - .addNextIntent(roomIntent) - - // android 4.3 issue - // use a generator for the private requestCode. - // When using 0, the intent is not created/launched when the user taps on the notification. - // - val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT) - - builder.setContentIntent(pendingIntent) - */ - return builder.build() }