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)
+ }
+}