Use self managed telecom manager mode for Android 8+
This commit is contained in:
parent
3aa1abc72c
commit
e341bb20e7
13 changed files with 649 additions and 12 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
105
app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt
Normal file
105
app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
194
app/src/main/java/org/linphone/telecom/TelecomHelper.kt
Normal file
194
app/src/main/java/org/linphone/telecom/TelecomHelper.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue