diff --git a/CHANGELOG.md b/CHANGELOG.md
index fffbe7094..fe66b8948 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ Group changes to describe their impact on the project, as follows:
- Reply to chat message feature (with original message preview)
- Voice recordings in chat feature
- Allow video recording in chat file sharing
+- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API
- New video call UI on foldable device like Galaxy Z Fold
- Setting to automatically record all calls
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a929545a0..8c5374fad 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,7 +5,7 @@
-
+
@@ -25,6 +25,9 @@
+
+
+
@@ -176,6 +179,14 @@
android:resource="@xml/authenticator" />
+
+
+
+
+
+
() {
private lateinit var viewModel: CallSettingsViewModel
@@ -83,16 +89,75 @@ class CallSettingsFragment : GenericSettingFragment
}
}
)
+
+ viewModel.enableTelecomManagerEvent.observe(
+ viewLifecycleOwner,
+ {
+ it.consume {
+ if (!PermissionHelper.get().hasTelecomManagerPermissions()) {
+ val permissions = arrayOf(
+ Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.MANAGE_OWN_CALLS
+ )
+ requestPermissions(permissions, 1)
+ } else if (!TelecomHelper.exists()) {
+ Log.w("[Telecom Helper] Doesn't exists yet, creating it")
+ TelecomHelper.create(requireContext())
+ }
+ }
+ }
+ )
+
+ viewModel.goToAndroidNotificationSettingsEvent.observe(
+ viewLifecycleOwner,
+ {
+ it.consume {
+ if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
+ val i = Intent()
+ i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
+ i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
+ i.putExtra(
+ Settings.EXTRA_CHANNEL_ID,
+ getString(R.string.notification_channel_service_id)
+ )
+ i.addCategory(Intent.CATEGORY_DEFAULT)
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
+ i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+ startActivity(i)
+ }
+ }
+ }
+ )
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- if (!Compatibility.canDrawOverlay(requireContext())) {
- viewModel.systemWideOverlayListener.onBoolValueChanged(false)
+ if (requestCode == 0 && !Compatibility.canDrawOverlay(requireContext())) {
+ viewModel.overlayListener.onBoolValueChanged(false)
+ } else if (requestCode == 1) {
+ if (!TelecomHelper.exists()) {
+ Log.w("[Telecom Helper] Doesn't exists yet, creating it")
+ TelecomHelper.create(requireContext())
+ }
+ updateTelecomManagerAccount()
}
}
+ private fun updateTelecomManagerAccount() {
+ if (!TelecomHelper.exists()) {
+ Log.e("[Telecom Helper] Doesn't exists, can't update account!")
+ return
+ }
+ // We have to refresh the account object otherwise isAccountEnabled will always return false...
+ val account = TelecomHelper.get().findExistingAccount(requireContext())
+ TelecomHelper.get().updateAccount(account)
+ val enabled = TelecomHelper.get().isAccountEnabled()
+ viewModel.useTelecomManager.value = enabled
+ corePreferences.useTelecomManager = enabled
+ }
+
override fun goBack() {
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
@@ -100,4 +165,22 @@ class CallSettingsFragment : GenericSettingFragment
navigateToEmptySetting()
}
}
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ for (index in grantResults.indices) {
+ val result = grantResults[index]
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ Log.w("[Call Settings] ${permissions[index]} permission denied but required for telecom manager")
+ viewModel.useTelecomManager.value = false
+ corePreferences.useTelecomManager = false
+ return
+ }
+ }
+
+ TelecomHelper.create(requireContext())
+ }
}
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt
index cc027a672..11bda5f63 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt
@@ -20,12 +20,13 @@
package org.linphone.activities.main.settings.viewmodels
import androidx.lifecycle.MutableLiveData
-import java.lang.NumberFormatException
import org.linphone.R
import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.core.MediaEncryption
import org.linphone.mediastream.Version
+import org.linphone.telecom.TelecomHelper
import org.linphone.utils.Event
+import org.linphone.utils.PermissionHelper
class CallSettingsViewModel : GenericSettingsViewModel() {
val deviceRingtoneListener = object : SettingListenerStub() {
@@ -62,6 +63,28 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
}
val encryptionMandatory = MutableLiveData()
+ val useTelecomManagerListener = object : SettingListenerStub() {
+ override fun onBoolValueChanged(newValue: Boolean) {
+ if (newValue &&
+ (
+ !PermissionHelper.get().hasTelecomManagerPermissions() ||
+ !TelecomHelper.exists() ||
+ !TelecomHelper.get().isAccountEnabled()
+ )
+ ) {
+ enableTelecomManagerEvent.value = Event(true)
+ } else {
+ if (!newValue && TelecomHelper.exists()) TelecomHelper.get().removeAccount()
+ prefs.useTelecomManager = newValue
+ }
+ }
+ }
+ val useTelecomManager = MutableLiveData()
+ val enableTelecomManagerEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+ val api26OrHigher = MutableLiveData()
+
val fullScreenListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
prefs.fullScreenCallUI = newValue
@@ -193,6 +216,9 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
initEncryptionList()
encryptionMandatory.value = core.isMediaEncryptionMandatory
+ useTelecomManager.value = prefs.useTelecomManager
+ api26OrHigher.value = Version.sdkAboveOrEqual(Version.API26_O_80)
+
fullScreen.value = prefs.fullScreenCallUI
overlay.value = prefs.showCallOverlay
systemWideOverlay.value = prefs.systemWideCallOverlay
diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt
index 79aabda87..81426b886 100644
--- a/app/src/main/java/org/linphone/core/CoreContext.kt
+++ b/app/src/main/java/org/linphone/core/CoreContext.kt
@@ -61,6 +61,7 @@ import org.linphone.contact.ContactsManager
import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
import org.linphone.notifications.NotificationsManager
+import org.linphone.telecom.TelecomHelper
import org.linphone.utils.*
import org.linphone.utils.Event
@@ -135,15 +136,17 @@ class CoreContext(val context: Context, coreConfig: Config) {
) {
Log.i("[Context] Call state changed [$state]")
if (state == Call.State.IncomingReceived || state == Call.State.IncomingEarlyMedia) {
- var gsmCallActive = false
- if (::phoneStateListener.isInitialized) {
- gsmCallActive = phoneStateListener.isInCall()
- }
+ if (!corePreferences.useTelecomManager) {
+ var gsmCallActive = false
+ if (::phoneStateListener.isInitialized) {
+ gsmCallActive = phoneStateListener.isInCall()
+ }
- if (gsmCallActive) {
- Log.w("[Context] Refusing the call with reason busy because a GSM call is active")
- call.decline(Reason.Busy)
- return
+ if (gsmCallActive) {
+ Log.w("[Context] Refusing the call with reason busy because a GSM call is active")
+ call.decline(Reason.Busy)
+ return
+ }
}
// Starting SDK 24 (Android 7.0) we rely on the fullscreen intent of the call incoming notification
@@ -294,6 +297,11 @@ class CoreContext(val context: Context, coreConfig: Config) {
notificationsManager.onCoreReady()
+ if (Version.sdkAboveOrEqual(Version.API26_O_80) && corePreferences.useTelecomManager) {
+ Log.i("[Context] Creating telecom helper")
+ TelecomHelper.create(context)
+ }
+
core.addListener(listener)
if (isPush) {
@@ -326,6 +334,10 @@ class CoreContext(val context: Context, coreConfig: Config) {
}
notificationsManager.destroy()
contactsManager.destroy()
+ if (TelecomHelper.exists()) {
+ Log.i("[Context] Destroying telecom helper")
+ TelecomHelper.get().destroy()
+ }
core.stop()
core.removeListener(listener)
diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt
index 7a6975ddf..72d2669eb 100644
--- a/app/src/main/java/org/linphone/core/CorePreferences.kt
+++ b/app/src/main/java/org/linphone/core/CorePreferences.kt
@@ -289,6 +289,13 @@ class CorePreferences constructor(private val context: Context) {
config.setBool("app", "auto_start_call_record", value)
}
+ var useTelecomManager: Boolean
+ // Some permissions are required, so keep it to false so user has to manually enable it and give permissions
+ get() = config.getBool("app", "use_self_managed_telecom_manager", false)
+ set(value) {
+ config.setBool("app", "use_self_managed_telecom_manager", value)
+ }
+
var fullScreenCallUI: Boolean
get() = config.getBool("app", "full_screen_call", true)
set(value) {
diff --git a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt
new file mode 100644
index 000000000..7df07f695
--- /dev/null
+++ b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2010-2021 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.graphics.drawable.Icon
+import android.os.Bundle
+import android.telecom.CallAudioState
+import android.telecom.Connection
+import android.telecom.DisconnectCause
+import android.telecom.StatusHints
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.core.Call
+import org.linphone.core.tools.Log
+import org.linphone.utils.AudioRouteUtils
+
+class NativeCallWrapper(var callId: String) : Connection() {
+ init {
+ val capabilities = connectionCapabilities or CAPABILITY_MUTE or CAPABILITY_SUPPORT_HOLD or CAPABILITY_HOLD
+ connectionCapabilities = capabilities
+ audioModeIsVoip = true
+ statusHints = StatusHints(
+ "",
+ Icon.createWithResource(coreContext.context, R.drawable.linphone_logo_tinted),
+ Bundle()
+ )
+ }
+
+ override fun onStateChanged(state: Int) {
+ Log.i("[Connection] Telecom state changed [$state] for call with id: $callId")
+ super.onStateChanged(state)
+ }
+
+ override fun onAnswer(videoState: Int) {
+ Log.i("[Connection] Answering telecom call with id: $callId")
+ getCall()?.accept()
+ }
+
+ override fun onHold() {
+ Log.i("[Connection] Pausing telecom call with id: $callId")
+ getCall()?.pause()
+ setOnHold()
+ }
+
+ override fun onUnhold() {
+ Log.i("[Connection] Resuming telecom call with id: $callId")
+ getCall()?.resume()
+ setActive()
+ }
+
+ override fun onCallAudioStateChanged(state: CallAudioState) {
+ Log.i("[Connection] Audio state changed: $state")
+
+ val call = getCall()
+ call?.microphoneMuted = state.isMuted
+ when (state.route) {
+ CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call)
+ CallAudioState.ROUTE_SPEAKER -> AudioRouteUtils.routeAudioToSpeaker(call)
+ CallAudioState.ROUTE_BLUETOOTH -> AudioRouteUtils.routeAudioToBluetooth(call)
+ CallAudioState.ROUTE_WIRED_HEADSET -> AudioRouteUtils.routeAudioToHeadset(call)
+ }
+ }
+
+ override fun onPlayDtmfTone(c: Char) {
+ Log.i("[Connection] Sending DTMF [$c] in telecom call with id: $callId")
+ getCall()?.sendDtmf(c)
+ }
+
+ override fun onDisconnect() {
+ Log.i("[Connection] Terminating telecom call with id: $callId")
+ getCall()?.terminate()
+ }
+
+ override fun onAbort() {
+ Log.i("[Connection] Aborting telecom call with id: $callId")
+ getCall()?.terminate()
+ }
+
+ override fun onReject() {
+ Log.i("[Connection] Rejecting telecom call with id: $callId")
+ setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
+ getCall()?.terminate()
+ }
+
+ private fun getCall(): Call? {
+ return coreContext.core.getCallByCallid(callId)
+ }
+}
diff --git a/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt b/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt
new file mode 100644
index 000000000..252bfbfda
--- /dev/null
+++ b/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2010-2021 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.ComponentName
+import android.content.Intent
+import android.net.Uri
+import android.telecom.*
+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
+
+class TelecomConnectionService : ConnectionService() {
+ private val connections = arrayListOf()
+
+ private val listener: CoreListenerStub = object : CoreListenerStub() {
+ override fun onCallStateChanged(
+ core: Core,
+ call: Call,
+ state: Call.State?,
+ message: String
+ ) {
+ Log.i("[Telecom Connection Service] call [${call.callLog.callId}] state changed: $state")
+ when (call.state) {
+ Call.State.OutgoingProgress -> {
+ for (connection in connections) {
+ if (connection.callId.isEmpty()) {
+ connection.callId = core.currentCall?.callLog?.callId ?: ""
+ }
+ }
+ }
+ Call.State.End, Call.State.Released -> onCallEnded(call)
+ Call.State.Connected -> onCallConnected(call)
+ }
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ Log.i("[Telecom Connection Service] onCreate()")
+ coreContext.core.addListener(listener)
+ }
+
+ override fun onUnbind(intent: Intent?): Boolean {
+ Log.i("[Telecom Connection Service] onUnbind()")
+ coreContext.core.removeListener(listener)
+
+ return super.onUnbind(intent)
+ }
+
+ override fun onCreateOutgoingConnection(
+ connectionManagerPhoneAccount: PhoneAccountHandle,
+ request: ConnectionRequest
+ ): Connection {
+ val accountHandle = request.accountHandle
+ val componentName = ComponentName(applicationContext, this.javaClass)
+
+ return if (accountHandle != null && componentName == accountHandle.componentName) {
+ Log.i("[Telecom Connection Service] Creating outgoing connection")
+
+ val extras = request.extras
+ var callId = extras.getString("Call-ID")
+ val displayName = extras.getString("DisplayName")
+ if (callId == null) {
+ callId = coreContext.core.currentCall?.callLog?.callId ?: ""
+ }
+ Log.i("[Telecom Connection Service] Outgoing connection is for call [$callId] with display name [$displayName]")
+
+ // Prevents user dialing back from native dialer app history
+ if (callId.isEmpty() && displayName.isNullOrEmpty()) {
+ Log.e("[Telecom Connection Service] Looks like a call was made from native dialer history, aborting")
+ return Connection.createFailedConnection(DisconnectCause(DisconnectCause.OTHER))
+ }
+
+ val connection = NativeCallWrapper(callId)
+ connection.setDialing()
+
+ val providedHandle = request.address
+ connection.setAddress(providedHandle, TelecomManager.PRESENTATION_ALLOWED)
+ connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
+ Log.i("[Telecom Connection Service] Address is $providedHandle")
+
+ connections.add(connection)
+ connection
+ } else {
+ Log.e("[Telecom Connection Service] Error: $accountHandle $componentName")
+ Connection.createFailedConnection(
+ DisconnectCause(
+ DisconnectCause.ERROR,
+ "Invalid inputs: $accountHandle $componentName"
+ )
+ )
+ }
+ }
+
+ override fun onCreateIncomingConnection(
+ connectionManagerPhoneAccount: PhoneAccountHandle,
+ request: ConnectionRequest
+ ): Connection {
+ val accountHandle = request.accountHandle
+ val componentName = ComponentName(applicationContext, this.javaClass)
+
+ return if (accountHandle != null && componentName == accountHandle.componentName) {
+ Log.i("[Telecom Connection Service] Creating incoming connection")
+
+ val extras = request.extras
+ val incomingExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS)
+ var callId = incomingExtras?.getString("Call-ID")
+ val displayName = incomingExtras?.getString("DisplayName")
+ if (callId == null) {
+ callId = coreContext.core.currentCall?.callLog?.callId ?: ""
+ }
+ Log.i("[Telecom Connection Service] Incoming connection is for call [$callId] with display name [$displayName]")
+
+ val connection = NativeCallWrapper(callId)
+ connection.setRinging()
+
+ val providedHandle =
+ incomingExtras?.getParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS)
+ connection.setAddress(providedHandle, TelecomManager.PRESENTATION_ALLOWED)
+ connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)
+ Log.i("[Telecom Connection Service] Address is $providedHandle")
+
+ connections.add(connection)
+ connection
+ } else {
+ Log.e("[Telecom Connection Service] Error: $accountHandle $componentName")
+ Connection.createFailedConnection(
+ DisconnectCause(
+ DisconnectCause.ERROR,
+ "Invalid inputs: $accountHandle $componentName"
+ )
+ )
+ }
+ }
+
+ private fun getConnectionForCallId(callId: String): NativeCallWrapper? {
+ return connections.find { connection ->
+ connection.callId == callId
+ }
+ }
+
+ private fun onCallEnded(call: Call) {
+ val connection = getConnectionForCallId(call.callLog.callId)
+ connection ?: return
+
+ connections.remove(connection)
+ connection.setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
+ connection.destroy()
+ }
+
+ private fun onCallConnected(call: Call) {
+ val connection = getConnectionForCallId(call.callLog.callId)
+ connection ?: return
+
+ if (connection.state != Connection.STATE_HOLDING) {
+ connection.setActive()
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt
new file mode 100644
index 000000000..6dfe178f5
--- /dev/null
+++ b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2010-2021 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.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.content.ComponentName
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Bundle
+import android.telecom.PhoneAccount
+import android.telecom.PhoneAccountHandle
+import android.telecom.TelecomManager
+import android.telecom.TelecomManager.*
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.contact.Contact
+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
+import org.linphone.utils.PermissionHelper
+import org.linphone.utils.SingletonHolder
+
+@TargetApi(26)
+class TelecomHelper private constructor(context: Context) {
+ companion object : SingletonHolder(::TelecomHelper)
+
+ private val telecomManager: TelecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
+
+ private var account: PhoneAccount = initPhoneAccount(context)
+
+ private val listener: CoreListenerStub = object : CoreListenerStub() {
+ override fun onFirstCallStarted(core: Core) {
+ val call = core.calls.firstOrNull()
+ call ?: return
+
+ if (call.dir == Call.Dir.Incoming) {
+ onIncomingCall(call)
+ } else {
+ onOutgoingCall(call)
+ }
+ }
+ }
+
+ init {
+ coreContext.core.addListener(listener)
+ Log.i("[Telecom Helper] Created")
+ }
+
+ fun destroy() {
+ coreContext.core.removeListener(listener)
+ Log.i("[Telecom Helper] Destroyed")
+ }
+
+ fun isAccountEnabled(): Boolean {
+ val enabled = account.isEnabled
+ Log.i("[Telecom Helper] Is account enabled ? $enabled")
+ return enabled
+ }
+
+ @SuppressLint("MissingPermission")
+ fun findExistingAccount(context: Context): PhoneAccount? {
+ if (PermissionHelper.get().hasReadPhoneState()) {
+ var account: PhoneAccount? = null
+ val phoneAccountHandleList: List =
+ telecomManager.selfManagedPhoneAccounts
+ val connectionService = ComponentName(context, TelecomConnectionService::class.java)
+ for (phoneAccountHandle in phoneAccountHandleList) {
+ val phoneAccount: PhoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle)
+ if (phoneAccountHandle.componentName == connectionService) {
+ Log.i("[Telecom Helper] Found existing phone account: $phoneAccount")
+ account = phoneAccount
+ break
+ }
+ }
+ return account
+ }
+ return null
+ }
+
+ fun updateAccount(newAccount: PhoneAccount?) {
+ if (newAccount != null) {
+ Log.i("[Telecom Helper] Updating account object: $newAccount")
+ account = newAccount
+ }
+ }
+
+ fun removeAccount() {
+ if (account.isEnabled) {
+ Log.w("[Telecom Helper] Unregistering phone account handler from telecom manager")
+ telecomManager.unregisterPhoneAccount(account.accountHandle)
+ } else {
+ Log.w("[Telecom Helper] Account wasn't enabled, skipping...")
+ }
+ }
+
+ private fun initPhoneAccount(context: Context): PhoneAccount {
+ val account: PhoneAccount? = findExistingAccount(context)
+ if (account == null) {
+ Log.i("[Telecom Helper] Phone account not found, let's create it")
+ return createAccount(context)
+ }
+ return account
+ }
+
+ private fun createAccount(context: Context): PhoneAccount {
+ val accountHandle = PhoneAccountHandle(
+ ComponentName(context, TelecomConnectionService::class.java),
+ context.packageName
+ )
+ val identity = coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly() ?: ""
+ val account = PhoneAccount.builder(accountHandle, context.getString(R.string.app_name))
+ .setAddress(Uri.parse(identity))
+ .setIcon(Icon.createWithResource(context, R.drawable.linphone_logo_tinted))
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+ .setHighlightColor(context.getColor(R.color.primary_color))
+ .setShortDescription(context.getString(R.string.app_description))
+ .setSupportedUriSchemes(listOf(PhoneAccount.SCHEME_SIP))
+ .build()
+
+ telecomManager.registerPhoneAccount(account)
+ Log.i("[Telecom Helper] Phone account created: $account")
+ return account
+ }
+
+ private fun onIncomingCall(call: Call) {
+ Log.i("[Telecom Helper] Incoming call received from ${call.remoteAddress.asStringUriOnly()}")
+
+ val extras = prepareBundle(call)
+ telecomManager.addNewIncomingCall(
+ account.accountHandle,
+ Bundle().apply {
+ putBundle(EXTRA_INCOMING_CALL_EXTRAS, extras)
+ putParcelable(EXTRA_PHONE_ACCOUNT_HANDLE, account.accountHandle)
+ }
+ )
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun onOutgoingCall(call: Call) {
+ Log.i("[Telecom Helper] Outgoing call started to ${call.remoteAddress.asStringUriOnly()}")
+
+ val extras = prepareBundle(call)
+ telecomManager.placeCall(
+ Uri.parse(call.remoteAddress.asStringUriOnly()),
+ Bundle().apply {
+ putBundle(EXTRA_OUTGOING_CALL_EXTRAS, extras)
+ putParcelable(EXTRA_PHONE_ACCOUNT_HANDLE, account.accountHandle)
+ }
+ )
+ }
+
+ private fun prepareBundle(call: Call): Bundle {
+ val extras = Bundle()
+ val address = call.remoteAddress
+
+ if (call.dir == Call.Dir.Outgoing) {
+ extras.putString(
+ EXTRA_CALL_BACK_NUMBER,
+ call.callLog.fromAddress.asStringUriOnly()
+ )
+ } else {
+ extras.putParcelable(EXTRA_INCOMING_CALL_ADDRESS, Uri.parse(address.asStringUriOnly()))
+ }
+
+ extras.putString("Call-ID", call.callLog.callId)
+
+ val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
+ val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
+ extras.putString("DisplayName", displayName)
+
+ return extras
+ }
+}
diff --git a/app/src/main/java/org/linphone/utils/PermissionHelper.kt b/app/src/main/java/org/linphone/utils/PermissionHelper.kt
index 9f1798501..7c3ecba67 100644
--- a/app/src/main/java/org/linphone/utils/PermissionHelper.kt
+++ b/app/src/main/java/org/linphone/utils/PermissionHelper.kt
@@ -69,4 +69,9 @@ class PermissionHelper private constructor(private val context: Context) {
fun hasRecordAudioPermission(): Boolean {
return hasPermission(Manifest.permission.RECORD_AUDIO)
}
+
+ fun hasTelecomManagerPermissions(): Boolean {
+ return hasPermission(Manifest.permission.READ_PHONE_STATE) &&
+ hasPermission(Manifest.permission.MANAGE_OWN_CALLS)
+ }
}
diff --git a/app/src/main/res/layout/settings_call_fragment.xml b/app/src/main/res/layout/settings_call_fragment.xml
index 454afce09..12caa303b 100644
--- a/app/src/main/res/layout/settings_call_fragment.xml
+++ b/app/src/main/res/layout/settings_call_fragment.xml
@@ -94,6 +94,15 @@
linphone:listener="@{viewModel.encryptionMandatoryListener}"
linphone:checked="@={viewModel.encryptionMandatory}"
linphone:enabled="@{viewModel.encryptionIndex != 0}" />
+
+
+
Enregistrer automatiquement les appels
Désactiver le mode sécurisé de l\'interface
Autorise l\'écran à être capturé/enregistré sur les vues sensibles
+ Améliore les intéractions avec les périphériques bluetooth
+ Nécessite des permissions supplémentaires
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf0e13d53..0c4c1d23b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -400,6 +400,8 @@
ZRTP
DTLS
Media encryption mandatory
+ Improve interactions with bluetooth devices
+ Requires some extra permissions
Full screen app while in call
Hides status and navigation bars
Overlay call notification