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