diff --git a/app/build.gradle b/app/build.gradle index 05c783511..45c711111 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,6 +30,7 @@ project.tasks['preBuild'].dependsOn 'linphoneSdkSource' android { namespace 'org.linphone' compileSdk 34 + compileSdkPreview 'UpsideDownCake' defaultConfig { applicationId packageName @@ -73,7 +74,9 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" - def nav_version = "2.7.0" + implementation "androidx.core:core-telecom:1.0.0-alpha01" + + def nav_version = "2.7.1" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 1746d0a94..3d4223e6d 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -125,8 +125,7 @@ class ContactsManager @UiThread constructor(context: Context) { } @WorkerThread - fun onCoreStarted() { - val core = coreContext.core + fun onCoreStarted(core: Core) { core.addListener(coreListener) for (list in core.friendsLists) { list.addListener(friendListListener) @@ -134,8 +133,7 @@ class ContactsManager @UiThread constructor(context: Context) { } @WorkerThread - fun onCoreStopped() { - val core = coreContext.core + fun onCoreStopped(core: Core) { core.removeListener(coreListener) for (list in core.friendsLists) { list.removeListener(friendListListener) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index bf10cb0ee..7b4dcffe7 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -36,6 +36,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.contacts.ContactsManager import org.linphone.core.tools.Log import org.linphone.notifications.NotificationsManager +import org.linphone.telecom.TelecomManager import org.linphone.ui.voip.VoipActivity import org.linphone.utils.ActivityMonitor import org.linphone.utils.LinphoneUtils @@ -53,6 +54,8 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C val notificationsManager = NotificationsManager(context) + private val telecomManager = TelecomManager(context) + private val activityMonitor = ActivityMonitor() private val mainThread = Handler(Looper.getMainLooper()) @@ -122,8 +125,9 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C core.start() - contactsManager.onCoreStarted() - notificationsManager.onCoreStarted() + contactsManager.onCoreStarted(core) + telecomManager.onCoreStarted(core) + notificationsManager.onCoreStarted(core) Looper.loop() } @@ -132,8 +136,9 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C override fun destroy() { core.stop() - contactsManager.onCoreStopped() - notificationsManager.onCoreStopped() + contactsManager.onCoreStopped(core) + telecomManager.onCoreStopped(core) + notificationsManager.onCoreStopped(core) postOnMainThread { (context as Application).unregisterActivityLifecycleCallbacks(activityMonitor) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 7a59e510a..e5fec45db 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -129,15 +129,15 @@ class NotificationsManager @MainThread constructor(private val context: Context) } @WorkerThread - fun onCoreStarted() { - coreContext.core.addListener(coreListener) + fun onCoreStarted(core: Core) { + Log.i("$TAG Core has been started") + core.addListener(coreListener) } @WorkerThread - fun onCoreStopped() { + fun onCoreStopped(core: Core) { Log.i("$TAG Getting destroyed, clearing foreground Service & call notifications") - - coreContext.core.removeListener(coreListener) + core.removeListener(coreListener) } @WorkerThread diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt new file mode 100644 index 000000000..25d1ab3bc --- /dev/null +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.telecom + +import android.telecom.DisconnectCause +import androidx.core.telecom.CallAttributesCompat +import androidx.core.telecom.CallControlCallback +import androidx.core.telecom.CallControlScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call +import org.linphone.core.CallListenerStub +import org.linphone.core.tools.Log + +class TelecomCallControlCallback constructor( + private val call: Call, + private val callControl: CallControlScope, + private val scope: CoroutineScope +) : CallControlCallback { + companion object { + private const val TAG = "[Telecom Call Control Callback]" + } + + private val callListener = object : CallListenerStub() { + override fun onStateChanged(call: Call, state: Call.State?, message: String) { + Log.i("$TAG Call state changed [$state]") + if (state == Call.State.Connected) { + if (call.dir == Call.Dir.Incoming) { + scope.launch { + Log.i("$TAG Answering call") + callControl.answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL) // TODO + } + } else { + scope.launch { + Log.i("$TAG Setting call active") + callControl.setActive() + } + } + } else if (state == Call.State.End) { + scope.launch { + Log.i("$TAG Disconnecting call") + callControl.disconnect(DisconnectCause(DisconnectCause.REMOTE)) + } + } else if (state == Call.State.Pausing) { + scope.launch { + Log.i("$TAG Pausing call") + callControl.setInactive() + } + } else if (state == Call.State.Resuming) { + scope.launch { + Log.i("$TAG Resuming call") + callControl.setActive() + } + } + } + } + + init { + // NEVER CALL ANY METHOD FROM callControl OBJECT IN HERE! + Log.i("$TAG Created callback for call") + coreContext.postOnCoreThread { + call.addListener(callListener) + } + } + + fun onCallControlCallbackSet() { + Log.i( + "$TAG Callback have been set for call, Telecom call ID is [${callControl.getCallId()}]" + ) + + callControl.availableEndpoints.onEach { list -> + Log.i("$TAG New available audio endpoints list") + for (endpoint in list) { + Log.i("$TAG Available audio endpoint [${endpoint.name}]") + } + }.launchIn(scope) + + callControl.currentCallEndpoint.onEach { endpoint -> + Log.i("$TAG We're asked to use [${endpoint.name}] audio endpoint") + }.launchIn(scope) + + callControl.isMuted.onEach { muted -> + Log.i("$TAG We're asked to ${if (muted) "mute" else "unmute"} the call") + call.microphoneMuted = muted + }.launchIn(scope) + } + + override suspend fun onAnswer(callType: Int): Boolean { + Log.i("$TAG We're asked to answer the call") + coreContext.postOnCoreThread { + if (call.state == Call.State.IncomingReceived || call.state == Call.State.IncomingEarlyMedia) { + Log.i("$TAG Answering call") + call.accept() + } + } + return true + } + + override suspend fun onDisconnect(disconnectCause: DisconnectCause): Boolean { + Log.i("$TAG We're asked to terminate the call with reason [$disconnectCause]") + coreContext.postOnCoreThread { + Log.i("$TAG Terminating call") + call.terminate() + } + return true + } + + override suspend fun onSetActive(): Boolean { + Log.i("$TAG We're asked to resume the call") + coreContext.postOnCoreThread { + Log.i("$TAG Resuming call") + call.resume() + } + return true + } + + override suspend fun onSetInactive(): Boolean { + Log.i("$TAG We're asked to pause the call") + coreContext.postOnCoreThread { + Log.i("$TAG Pausing call") + call.pause() + } + return true + } +} diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt new file mode 100644 index 000000000..6cc73ac42 --- /dev/null +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.telecom + +import android.content.Context +import android.net.Uri +import androidx.annotation.WorkerThread +import androidx.core.telecom.CallAttributesCompat +import androidx.core.telecom.CallsManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.tools.Log +import org.linphone.utils.LinphoneUtils + +class TelecomManager @WorkerThread constructor(context: Context) { + companion object { + private const val TAG = "[Telecom Manager]" + } + + private val callsManager = CallsManager(context) + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private val map = HashMap() + + private val coreListener = object : CoreListenerStub() { + @WorkerThread + override fun onCallCreated(core: Core, call: Call) { + Log.i("$TAG Call created: $call") + + val address = call.remoteAddress + val friend = coreContext.contactsManager.findContactByAddress(address) + val displayName = friend?.name ?: LinphoneUtils.getDisplayName(address) + val uri = Uri.parse(address.asStringUriOnly()) + val direction = if (call.dir == Call.Dir.Outgoing) { + CallAttributesCompat.DIRECTION_OUTGOING + } else { + CallAttributesCompat.DIRECTION_INCOMING + } + val type = CallAttributesCompat.CALL_TYPE_AUDIO_CALL or CallAttributesCompat.CALL_TYPE_VIDEO_CALL + val capabilities = CallAttributesCompat.SUPPORTS_SET_INACTIVE or CallAttributesCompat.SUPPORTS_TRANSFER + + val callAttributes = CallAttributesCompat( + displayName, + uri, + direction, + type, + capabilities + ) + scope.launch { + callsManager.addCall(callAttributes) { + val callbacks = TelecomCallControlCallback(call, this, scope) + + coreContext.postOnCoreThread { + val callId = call.callLog.callId.orEmpty() + if (callId.isNotEmpty()) { + Log.i("$TAG Storing callbacks (why?) for call ID [$callId]") + map[callId] = callbacks + } + } + + setCallback(callbacks) + // We must first call setCallback on callControlScope before using it + callbacks.onCallControlCallbackSet() + } + } + } + } + + init { + callsManager.registerAppWithTelecom( + CallsManager.Companion.CAPABILITY_SUPPORTS_VIDEO_CALLING + ) + Log.i("$TAG App has been registered with Telecom") + } + + @WorkerThread + fun onCoreStarted(core: Core) { + Log.i("$TAG Core has been started") + core.addListener(coreListener) + } + + @WorkerThread + fun onCoreStopped(core: Core) { + Log.i("$TAG Core is being stopped") + core.removeListener(coreListener) + } +}