Use self managed telecom manager mode for Android 8+

This commit is contained in:
Sylvain Berfini 2021-09-24 10:09:13 +02:00
parent 3aa1abc72c
commit e341bb20e7
13 changed files with 649 additions and 12 deletions

View file

@ -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

View file

@ -5,7 +5,7 @@
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<!-- Helps filling phone number and country code in assistant -->
<!-- Helps filling phone number and country code in assistant & for Telecom Manager -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- Needed for auto start at boot and to ensure the service won't be killed by OS while in call -->
@ -25,6 +25,9 @@
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- Needed for Telecom Manager -->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<!-- Needed for overlay -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
@ -176,6 +179,14 @@
android:resource="@xml/authenticator" />
</service>
<service android:name=".telecom.TelecomConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<!-- Receivers -->
<receiver android:name=".core.CorePushReceiver"

View file

@ -19,20 +19,26 @@
*/
package org.linphone.activities.main.settings.fragments
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.View
import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.main.settings.viewmodels.CallSettingsViewModel
import org.linphone.activities.navigateToEmptySetting
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.databinding.SettingsCallFragmentBinding
import org.linphone.mediastream.Version
import org.linphone.telecom.TelecomHelper
import org.linphone.utils.Event
import org.linphone.utils.PermissionHelper
class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>() {
private lateinit var viewModel: CallSettingsViewModel
@ -83,16 +89,75 @@ class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>
}
}
)
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<SettingsCallFragmentBinding>
navigateToEmptySetting()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
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())
}
}

View file

@ -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<Boolean>()
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<Boolean>()
val enableTelecomManagerEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val api26OrHigher = MutableLiveData<Boolean>()
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

View file

@ -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)

View file

@ -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) {

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<NativeCallWrapper>()
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<Uri>(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()
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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, Context>(::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<PhoneAccountHandle> =
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
}
}

View file

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

View file

@ -94,6 +94,15 @@
linphone:listener="@{viewModel.encryptionMandatoryListener}"
linphone:checked="@={viewModel.encryptionMandatory}"
linphone:enabled="@{viewModel.encryptionIndex != 0}" />
<include
layout="@layout/settings_widget_switch"
linphone:title="@{@string/call_settings_use_telecom_manager_title}"
linphone:subtitle="@{@string/call_settings_use_telecom_manager_summary}"
android:visibility="@{viewModel.api26OrHigher ? View.VISIBLE : View.GONE}"
linphone:listener="@{viewModel.useTelecomManagerListener}"
linphone:checked="@={viewModel.useTelecomManager}"/>
<include
layout="@layout/settings_widget_switch"
linphone:title="@{@string/call_settings_full_screen_title}"

View file

@ -622,4 +622,6 @@
<string name="call_settings_auto_start_recording_title">Enregistrer automatiquement les appels</string>
<string name="advanced_settings_disable_fragment_security_title">Désactiver le mode sécurisé de l\'interface</string>
<string name="advanced_settings_disable_fragment_security_summary">Autorise l\'écran à être capturé/enregistré sur les vues sensibles</string>
<string name="call_settings_use_telecom_manager_title">Améliore les intéractions avec les périphériques bluetooth</string>
<string name="call_settings_use_telecom_manager_summary">Nécessite des permissions supplémentaires</string>
</resources>

View file

@ -400,6 +400,8 @@
<string name="call_settings_media_encryption_zrtp">ZRTP</string>
<string name="call_settings_media_encryption_dtls">DTLS</string>
<string name="call_settings_encryption_mandatory_title">Media encryption mandatory</string>
<string name="call_settings_use_telecom_manager_title">Improve interactions with bluetooth devices</string>
<string name="call_settings_use_telecom_manager_summary">Requires some extra permissions</string>
<string name="call_settings_full_screen_title">Full screen app while in call</string>
<string name="call_settings_full_screen_summary">Hides status and navigation bars</string>
<string name="call_settings_overlay_title">Overlay call notification</string>