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) - Reply to chat message feature (with original message preview)
- Voice recordings in chat feature - Voice recordings in chat feature
- Allow video recording in chat file sharing - 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 - New video call UI on foldable device like Galaxy Z Fold
- Setting to automatically record all calls - 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.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_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" /> <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 --> <!-- 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.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_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 --> <!-- Needed for overlay -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
@ -176,6 +179,14 @@
android:resource="@xml/authenticator" /> android:resource="@xml/authenticator" />
</service> </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 --> <!-- Receivers -->
<receiver android:name=".core.CorePushReceiver" <receiver android:name=".core.CorePushReceiver"

View file

@ -19,20 +19,26 @@
*/ */
package org.linphone.activities.main.settings.fragments package org.linphone.activities.main.settings.fragments
import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.activities.main.settings.viewmodels.CallSettingsViewModel import org.linphone.activities.main.settings.viewmodels.CallSettingsViewModel
import org.linphone.activities.navigateToEmptySetting import org.linphone.activities.navigateToEmptySetting
import org.linphone.compatibility.Compatibility import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.databinding.SettingsCallFragmentBinding import org.linphone.databinding.SettingsCallFragmentBinding
import org.linphone.mediastream.Version import org.linphone.mediastream.Version
import org.linphone.telecom.TelecomHelper
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.PermissionHelper
class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>() { class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>() {
private lateinit var viewModel: CallSettingsViewModel 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (!Compatibility.canDrawOverlay(requireContext())) { if (requestCode == 0 && !Compatibility.canDrawOverlay(requireContext())) {
viewModel.systemWideOverlayListener.onBoolValueChanged(false) 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() { override fun goBack() {
if (sharedViewModel.isSlidingPaneSlideable.value == true) { if (sharedViewModel.isSlidingPaneSlideable.value == true) {
sharedViewModel.closeSlidingPaneEvent.value = Event(true) sharedViewModel.closeSlidingPaneEvent.value = Event(true)
@ -100,4 +165,22 @@ class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>
navigateToEmptySetting() 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 package org.linphone.activities.main.settings.viewmodels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import java.lang.NumberFormatException
import org.linphone.R import org.linphone.R
import org.linphone.activities.main.settings.SettingListenerStub import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.core.MediaEncryption import org.linphone.core.MediaEncryption
import org.linphone.mediastream.Version import org.linphone.mediastream.Version
import org.linphone.telecom.TelecomHelper
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.PermissionHelper
class CallSettingsViewModel : GenericSettingsViewModel() { class CallSettingsViewModel : GenericSettingsViewModel() {
val deviceRingtoneListener = object : SettingListenerStub() { val deviceRingtoneListener = object : SettingListenerStub() {
@ -62,6 +63,28 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
} }
val encryptionMandatory = MutableLiveData<Boolean>() 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() { val fullScreenListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
prefs.fullScreenCallUI = newValue prefs.fullScreenCallUI = newValue
@ -193,6 +216,9 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
initEncryptionList() initEncryptionList()
encryptionMandatory.value = core.isMediaEncryptionMandatory encryptionMandatory.value = core.isMediaEncryptionMandatory
useTelecomManager.value = prefs.useTelecomManager
api26OrHigher.value = Version.sdkAboveOrEqual(Version.API26_O_80)
fullScreen.value = prefs.fullScreenCallUI fullScreen.value = prefs.fullScreenCallUI
overlay.value = prefs.showCallOverlay overlay.value = prefs.showCallOverlay
systemWideOverlay.value = prefs.systemWideCallOverlay systemWideOverlay.value = prefs.systemWideCallOverlay

View file

@ -61,6 +61,7 @@ import org.linphone.contact.ContactsManager
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.mediastream.Version import org.linphone.mediastream.Version
import org.linphone.notifications.NotificationsManager import org.linphone.notifications.NotificationsManager
import org.linphone.telecom.TelecomHelper
import org.linphone.utils.* import org.linphone.utils.*
import org.linphone.utils.Event import org.linphone.utils.Event
@ -135,15 +136,17 @@ class CoreContext(val context: Context, coreConfig: Config) {
) { ) {
Log.i("[Context] Call state changed [$state]") Log.i("[Context] Call state changed [$state]")
if (state == Call.State.IncomingReceived || state == Call.State.IncomingEarlyMedia) { if (state == Call.State.IncomingReceived || state == Call.State.IncomingEarlyMedia) {
var gsmCallActive = false if (!corePreferences.useTelecomManager) {
if (::phoneStateListener.isInitialized) { var gsmCallActive = false
gsmCallActive = phoneStateListener.isInCall() if (::phoneStateListener.isInitialized) {
} gsmCallActive = phoneStateListener.isInCall()
}
if (gsmCallActive) { if (gsmCallActive) {
Log.w("[Context] Refusing the call with reason busy because a GSM call is active") Log.w("[Context] Refusing the call with reason busy because a GSM call is active")
call.decline(Reason.Busy) call.decline(Reason.Busy)
return return
}
} }
// Starting SDK 24 (Android 7.0) we rely on the fullscreen intent of the call incoming notification // 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() notificationsManager.onCoreReady()
if (Version.sdkAboveOrEqual(Version.API26_O_80) && corePreferences.useTelecomManager) {
Log.i("[Context] Creating telecom helper")
TelecomHelper.create(context)
}
core.addListener(listener) core.addListener(listener)
if (isPush) { if (isPush) {
@ -326,6 +334,10 @@ class CoreContext(val context: Context, coreConfig: Config) {
} }
notificationsManager.destroy() notificationsManager.destroy()
contactsManager.destroy() contactsManager.destroy()
if (TelecomHelper.exists()) {
Log.i("[Context] Destroying telecom helper")
TelecomHelper.get().destroy()
}
core.stop() core.stop()
core.removeListener(listener) core.removeListener(listener)

View file

@ -289,6 +289,13 @@ class CorePreferences constructor(private val context: Context) {
config.setBool("app", "auto_start_call_record", value) 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 var fullScreenCallUI: Boolean
get() = config.getBool("app", "full_screen_call", true) get() = config.getBool("app", "full_screen_call", true)
set(value) { 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 { fun hasRecordAudioPermission(): Boolean {
return hasPermission(Manifest.permission.RECORD_AUDIO) 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:listener="@{viewModel.encryptionMandatoryListener}"
linphone:checked="@={viewModel.encryptionMandatory}" linphone:checked="@={viewModel.encryptionMandatory}"
linphone:enabled="@{viewModel.encryptionIndex != 0}" /> 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 <include
layout="@layout/settings_widget_switch" layout="@layout/settings_widget_switch"
linphone:title="@{@string/call_settings_full_screen_title}" 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="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_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="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> </resources>

View file

@ -400,6 +400,8 @@
<string name="call_settings_media_encryption_zrtp">ZRTP</string> <string name="call_settings_media_encryption_zrtp">ZRTP</string>
<string name="call_settings_media_encryption_dtls">DTLS</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_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_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_full_screen_summary">Hides status and navigation bars</string>
<string name="call_settings_overlay_title">Overlay call notification</string> <string name="call_settings_overlay_title">Overlay call notification</string>