mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Reintroduce preliminary telecom support for 1:1 calling.
This commit is contained in:
committed by
Michelle Tang
parent
1b6cfe9fc6
commit
a62f07db11
@@ -16,6 +16,7 @@ import org.signal.core.util.PendingIntentFlags;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.webrtc.ActiveCallManager;
|
||||
@@ -93,7 +94,7 @@ public class CallNotificationBuilder {
|
||||
.setSmallIcon(R.drawable.ic_call_secure_white_24dp)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setContentTitle(recipient.getDisplayName(context));
|
||||
.setContentTitle(SignalStore.settings().getMessageNotificationsPrivacy().isDisplayContact() ? recipient.getDisplayName(context) : context.getString(R.string.Recipient_signal_call));
|
||||
|
||||
if (type == TYPE_INCOMING_CONNECTING) {
|
||||
builder.setContentText(context.getString(R.string.CallNotificationBuilder_connecting));
|
||||
@@ -106,8 +107,15 @@ public class CallNotificationBuilder {
|
||||
builder.setCategory(NotificationCompat.CATEGORY_CALL);
|
||||
builder.setFullScreenIntent(pendingIntent, true);
|
||||
|
||||
Person person = skipPersonIcon ? ConversationUtil.buildPersonWithoutIcon(context, recipient)
|
||||
: ConversationUtil.buildPerson(context.getApplicationContext(), recipient);
|
||||
Person person;
|
||||
|
||||
if (SignalStore.settings().getMessageNotificationsPrivacy().isDisplayContact()) {
|
||||
person = skipPersonIcon ? ConversationUtil.buildPersonWithoutIcon(context, recipient)
|
||||
: ConversationUtil.buildPerson(context.getApplicationContext(), recipient);
|
||||
} else {
|
||||
person = new Person.Builder().setName(context.getString(R.string.Recipient_signal_call))
|
||||
.build();
|
||||
}
|
||||
|
||||
builder.addPerson(person);
|
||||
|
||||
@@ -130,8 +138,15 @@ public class CallNotificationBuilder {
|
||||
builder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
||||
builder.setCategory(NotificationCompat.CATEGORY_CALL);
|
||||
|
||||
Person person = skipPersonIcon ? ConversationUtil.buildPersonWithoutIcon(context, recipient)
|
||||
: ConversationUtil.buildPerson(context.getApplicationContext(), recipient);
|
||||
Person person;
|
||||
|
||||
if (SignalStore.settings().getMessageNotificationsPrivacy().isDisplayContact()) {
|
||||
person = skipPersonIcon ? ConversationUtil.buildPersonWithoutIcon(context, recipient)
|
||||
: ConversationUtil.buildPerson(context.getApplicationContext(), recipient);
|
||||
} else {
|
||||
person = new Person.Builder().setName(context.getString(R.string.Recipient_signal_call))
|
||||
.build();
|
||||
}
|
||||
|
||||
builder.addPerson(person);
|
||||
|
||||
|
||||
@@ -20,10 +20,14 @@ sealed class AudioManagerCommand : Parcelable {
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) = Unit
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
class Initialize : AudioManagerCommand() {
|
||||
class Initialize(val isGroupCall: Boolean = false) : AudioManagerCommand() {
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
ParcelUtil.writeBoolean(parcel, isGroupCall)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<Initialize> = ParcelCheat { Initialize() }
|
||||
val CREATOR: Parcelable.Creator<Initialize> = ParcelCheat { Initialize(ParcelUtil.readBoolean(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioRecordingConfiguration
|
||||
import android.media.MediaRecorder
|
||||
import android.net.Uri
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -22,9 +21,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
|
||||
private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE
|
||||
private var userSelectedAudioDevice: AudioDeviceInfo? = null
|
||||
private var savedAudioMode = AudioManager.MODE_INVALID
|
||||
private var savedIsSpeakerPhoneOn = false
|
||||
private var savedIsMicrophoneMute = false
|
||||
private var hasWiredHeadset = false
|
||||
|
||||
private val deviceCallback = object : AudioDeviceCallback() {
|
||||
@@ -207,32 +204,12 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
updateAudioDeviceState()
|
||||
}
|
||||
|
||||
override fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
|
||||
Log.i(TAG, "startIncomingRinger: uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate currentMode: ${getModeName(androidAudioManager.mode)}")
|
||||
androidAudioManager.mode = AudioManager.MODE_RINGTONE
|
||||
setMicrophoneMute(false)
|
||||
incomingRinger.start(ringtoneUri, vibrate)
|
||||
}
|
||||
|
||||
override fun startOutgoingRinger() {
|
||||
Log.i(TAG, "startOutgoingRinger: currentDevice: $selectedAudioDevice currentMode: ${getModeName(androidAudioManager.mode)}")
|
||||
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
setMicrophoneMute(false)
|
||||
outgoingRinger.start(OutgoingRinger.Type.RINGING)
|
||||
}
|
||||
|
||||
private fun setSpeakerphoneOn(on: Boolean) {
|
||||
if (androidAudioManager.isSpeakerphoneOn != on) {
|
||||
androidAudioManager.isSpeakerphoneOn = on
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMicrophoneMute(on: Boolean) {
|
||||
if (androidAudioManager.isMicrophoneMute != on) {
|
||||
androidAudioManager.isMicrophoneMute = on
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAudioDeviceState() {
|
||||
handler.assertHandlerThread()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.webrtc.audio
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.audio.AudioDeviceUpdatedListener
|
||||
import org.thoughtcrime.securesms.audio.SignalBluetoothManager
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil
|
||||
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
@@ -46,10 +48,16 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
|
||||
|
||||
private val stateChangeUpSoundId = soundPool.load(context, R.raw.notification_simple_01, 1)
|
||||
|
||||
protected var savedAudioMode = AudioManager.MODE_INVALID
|
||||
protected var savedIsMicrophoneMute = false
|
||||
|
||||
companion object {
|
||||
@SuppressLint("NewApi")
|
||||
@JvmStatic
|
||||
fun create(context: Context, eventListener: EventListener?): SignalAudioManager {
|
||||
return if (Build.VERSION.SDK_INT >= 31) {
|
||||
fun create(context: Context, eventListener: EventListener?, canUseTelecom: Boolean): SignalAudioManager {
|
||||
return if (canUseTelecom && AndroidTelecomUtil.telecomSupported) {
|
||||
TelecomAudioManager(context, eventListener)
|
||||
} else if (Build.VERSION.SDK_INT >= 31) {
|
||||
FullSignalAudioManagerApi31(context, eventListener)
|
||||
} else {
|
||||
FullSignalAudioManager(context, eventListener)
|
||||
@@ -94,14 +102,32 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
|
||||
protected abstract fun stop(playDisconnect: Boolean)
|
||||
protected abstract fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean)
|
||||
protected abstract fun selectAudioDevice(recipientId: RecipientId?, device: Int, isId: Boolean)
|
||||
protected abstract fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean)
|
||||
protected abstract fun startOutgoingRinger()
|
||||
|
||||
protected open fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
|
||||
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate")
|
||||
androidAudioManager.mode = AudioManager.MODE_RINGTONE
|
||||
setMicrophoneMute(false)
|
||||
incomingRinger.start(ringtoneUri, vibrate)
|
||||
}
|
||||
|
||||
protected open fun startOutgoingRinger() {
|
||||
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice")
|
||||
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
setMicrophoneMute(false)
|
||||
outgoingRinger.start(OutgoingRinger.Type.RINGING)
|
||||
}
|
||||
|
||||
protected open fun silenceIncomingRinger() {
|
||||
Log.i(TAG, "silenceIncomingRinger():")
|
||||
incomingRinger.stop()
|
||||
}
|
||||
|
||||
protected fun setMicrophoneMute(on: Boolean) {
|
||||
if (androidAudioManager.isMicrophoneMute != on) {
|
||||
androidAudioManager.isMicrophoneMute = on
|
||||
}
|
||||
}
|
||||
|
||||
enum class AudioDevice {
|
||||
SPEAKER_PHONE,
|
||||
WIRED_HEADSET,
|
||||
@@ -168,9 +194,7 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
|
||||
private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE
|
||||
private var previousBluetoothState: SignalBluetoothManager.State? = null
|
||||
|
||||
private var savedAudioMode = AudioManager.MODE_INVALID
|
||||
private var savedIsSpeakerPhoneOn = false
|
||||
private var savedIsMicrophoneMute = false
|
||||
private var hasWiredHeadset = false
|
||||
private var autoSwitchToWiredHeadset = true
|
||||
private var autoSwitchToBluetooth = true
|
||||
@@ -419,29 +443,6 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMicrophoneMute(on: Boolean) {
|
||||
if (androidAudioManager.isMicrophoneMute != on) {
|
||||
androidAudioManager.isMicrophoneMute = on
|
||||
}
|
||||
}
|
||||
|
||||
override fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
|
||||
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate")
|
||||
androidAudioManager.mode = AudioManager.MODE_RINGTONE
|
||||
setMicrophoneMute(false)
|
||||
|
||||
incomingRinger.start(ringtoneUri, vibrate)
|
||||
}
|
||||
|
||||
override fun startOutgoingRinger() {
|
||||
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice")
|
||||
|
||||
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
setMicrophoneMute(false)
|
||||
|
||||
outgoingRinger.start(OutgoingRinger.Type.RINGING)
|
||||
}
|
||||
|
||||
private fun onWiredHeadsetChange(pluggedIn: Boolean, hasMic: Boolean) {
|
||||
Log.i(TAG, "onWiredHeadsetChange state: $state plug: $pluggedIn mic: $hasMic")
|
||||
hasWiredHeadset = pluggedIn
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.webrtc.audio
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil
|
||||
|
||||
/**
|
||||
* Lightweight [SignalAudioManager] used when Jetpack Core Telecom is managing the call.
|
||||
*
|
||||
* Core Telecom owns device routing (earpiece, speaker, bluetooth, wired headset) and audio focus
|
||||
* via the platform telecom framework. This manager only handles:
|
||||
* - Audio mode transitions (MODE_RINGTONE / MODE_IN_COMMUNICATION)
|
||||
* - Ringtone and sound effect playback
|
||||
* - Mic mute state
|
||||
* - Forwarding user device selection to Core Telecom via [AndroidTelecomUtil]
|
||||
*
|
||||
* Device availability and active device updates flow from [org.thoughtcrime.securesms.service.webrtc.TelecomCallController] directly
|
||||
* to [org.thoughtcrime.securesms.service.webrtc.SignalCallManager.onAudioDeviceChanged], bypassing this class entirely.
|
||||
*/
|
||||
@RequiresApi(34)
|
||||
class TelecomAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(TelecomAudioManager::class)
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
Log.i(TAG, "initialize(): state=$state")
|
||||
if (state == State.UNINITIALIZED) {
|
||||
savedAudioMode = androidAudioManager.mode
|
||||
savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute
|
||||
setMicrophoneMute(false)
|
||||
state = State.PREINITIALIZED
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
Log.i(TAG, "start(): state=$state")
|
||||
if (state == State.RUNNING) {
|
||||
Log.w(TAG, "Skipping, already active")
|
||||
return
|
||||
}
|
||||
|
||||
incomingRinger.stop()
|
||||
outgoingRinger.stop()
|
||||
|
||||
state = State.RUNNING
|
||||
|
||||
Log.i(TAG, "start(): platform audio mode is ${androidAudioManager.mode}, not overriding — letting telecom framework manage")
|
||||
|
||||
val volume: Float = androidAudioManager.ringVolumeWithMinimum()
|
||||
soundPool.play(connectedSoundId, volume, volume, 0, 0, 1.0f)
|
||||
}
|
||||
|
||||
override fun stop(playDisconnect: Boolean) {
|
||||
Log.i(TAG, "stop(): playDisconnect=$playDisconnect state=$state")
|
||||
|
||||
incomingRinger.stop()
|
||||
outgoingRinger.stop()
|
||||
|
||||
if (playDisconnect && state != State.UNINITIALIZED) {
|
||||
val volume: Float = androidAudioManager.ringVolumeWithMinimum()
|
||||
soundPool.play(disconnectedSoundId, volume, volume, 0, 0, 1.0f)
|
||||
}
|
||||
|
||||
if (state != State.UNINITIALIZED) {
|
||||
setMicrophoneMute(savedIsMicrophoneMute)
|
||||
}
|
||||
|
||||
state = State.UNINITIALIZED
|
||||
}
|
||||
|
||||
override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
|
||||
if (recipientId != null) {
|
||||
val currentDevice = AndroidTelecomUtil.getActiveAudioDevice(recipientId)
|
||||
if (currentDevice == AudioDevice.BLUETOOTH || currentDevice == AudioDevice.WIRED_HEADSET) {
|
||||
Log.i(TAG, "setDefaultAudioDevice(): device=$newDefaultDevice, but current device is $currentDevice — keeping external device")
|
||||
return
|
||||
}
|
||||
|
||||
if (newDefaultDevice == AudioDevice.EARPIECE) {
|
||||
Log.i(TAG, "setDefaultAudioDevice(): device=EARPIECE — no-op, letting telecom framework decide default routing")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "setDefaultAudioDevice(): device=$newDefaultDevice (delegating to telecom)")
|
||||
AndroidTelecomUtil.selectAudioDevice(recipientId, newDefaultDevice)
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectAudioDevice(recipientId: RecipientId?, device: Int, isId: Boolean) {
|
||||
val audioDevice: AudioDevice = if (isId) {
|
||||
Log.w(TAG, "selectAudioDevice(): unexpected isId=true for telecom call, ignoring")
|
||||
return
|
||||
} else {
|
||||
AudioDevice.entries[device]
|
||||
}
|
||||
|
||||
Log.i(TAG, "selectAudioDevice(): device=$audioDevice (delegating to telecom)")
|
||||
if (recipientId != null) {
|
||||
AndroidTelecomUtil.selectAudioDevice(recipientId, audioDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user