use call integration via MANAGE_OWN_CALLS
better integrate calls into the system via 'Build a calling app'¹ a few hooks like onAnswer/onReject and automatic PhoneAccount creation are still missing ¹: https://developer.android.com/develop/connectivity/telecom/selfManaged
This commit is contained in:
parent
aefcce430d
commit
19c634f3d2
|
@ -95,7 +95,7 @@ android {
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 21
|
minSdkVersion 23
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode 42094
|
versionCode 42094
|
||||||
versionName "2.13.4"
|
versionName "2.13.4"
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.READ_PHONE_STATE"
|
|
||||||
android:maxSdkVersion="22" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
@ -50,6 +47,8 @@
|
||||||
<!-- this foreground service type permission is exclusively used for import and export backup -->
|
<!-- this foreground service type permission is exclusively used for import and export backup -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
@ -133,6 +132,14 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service android:name=".services.CallIntegrationConnectionService"
|
||||||
|
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.telecom.ConnectionService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".services.EventReceiver"
|
android:name=".services.EventReceiver"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
|
|
|
@ -58,18 +58,18 @@ public class AppRTCAudioManager {
|
||||||
private boolean hasWiredHeadset;
|
private boolean hasWiredHeadset;
|
||||||
// Default audio device; speaker phone for video calls or earpiece for audio
|
// Default audio device; speaker phone for video calls or earpiece for audio
|
||||||
// only calls.
|
// only calls.
|
||||||
private AudioDevice defaultAudioDevice;
|
private CallIntegration.AudioDevice defaultAudioDevice;
|
||||||
// Contains the currently selected audio device.
|
// Contains the currently selected audio device.
|
||||||
// This device is changed automatically using a certain scheme where e.g.
|
// This device is changed automatically using a certain scheme where e.g.
|
||||||
// a wired headset "wins" over speaker phone. It is also possible for a
|
// a wired headset "wins" over speaker phone. It is also possible for a
|
||||||
// user to explicitly select a device (and overrid any predefined scheme).
|
// user to explicitly select a device (and overrid any predefined scheme).
|
||||||
// See |userSelectedAudioDevice| for details.
|
// See |userSelectedAudioDevice| for details.
|
||||||
private AudioDevice selectedAudioDevice;
|
private CallIntegration.AudioDevice selectedAudioDevice;
|
||||||
// Contains the user-selected audio device which overrides the predefined
|
// Contains the user-selected audio device which overrides the predefined
|
||||||
// selection scheme.
|
// selection scheme.
|
||||||
// TODO(henrika): always set to AudioDevice.NONE today. Add support for
|
// TODO(henrika): always set to AudioDevice.NONE today. Add support for
|
||||||
// explicit selection based on choice by userSelectedAudioDevice.
|
// explicit selection based on choice by userSelectedAudioDevice.
|
||||||
private AudioDevice userSelectedAudioDevice;
|
private CallIntegration.AudioDevice userSelectedAudioDevice;
|
||||||
// Proximity sensor object. It measures the proximity of an object in cm
|
// Proximity sensor object. It measures the proximity of an object in cm
|
||||||
// relative to the view screen of a device and can therefore be used to
|
// relative to the view screen of a device and can therefore be used to
|
||||||
// assist device switching (close to ear <=> use headset earpiece if
|
// assist device switching (close to ear <=> use headset earpiece if
|
||||||
|
@ -78,26 +78,25 @@ public class AppRTCAudioManager {
|
||||||
private AppRTCProximitySensor proximitySensor;
|
private AppRTCProximitySensor proximitySensor;
|
||||||
// Contains a list of available audio devices. A Set collection is used to
|
// Contains a list of available audio devices. A Set collection is used to
|
||||||
// avoid duplicate elements.
|
// avoid duplicate elements.
|
||||||
private Set<AudioDevice> audioDevices = new HashSet<>();
|
private Set<CallIntegration.AudioDevice> audioDevices = new HashSet<>();
|
||||||
// Broadcast receiver for wired headset intent broadcasts.
|
// Broadcast receiver for wired headset intent broadcasts.
|
||||||
private final BroadcastReceiver wiredHeadsetReceiver;
|
private final BroadcastReceiver wiredHeadsetReceiver;
|
||||||
// Callback method for changes in audio focus.
|
// Callback method for changes in audio focus.
|
||||||
@Nullable
|
@Nullable
|
||||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||||
|
|
||||||
private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) {
|
public AppRTCAudioManager(final Context context) {
|
||||||
Log.d(Config.LOGTAG, "ctor");
|
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
apprtcContext = context;
|
apprtcContext = context;
|
||||||
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
|
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
|
||||||
bluetoothManager = AppRTCBluetoothManager.create(context, this);
|
bluetoothManager = AppRTCBluetoothManager.create(context, this);
|
||||||
wiredHeadsetReceiver = new WiredHeadsetReceiver();
|
wiredHeadsetReceiver = new WiredHeadsetReceiver();
|
||||||
amState = AudioManagerState.UNINITIALIZED;
|
amState = AudioManagerState.UNINITIALIZED;
|
||||||
this.speakerPhonePreference = speakerPhonePreference;
|
// CallIntegration / Connection uses Earpiece as default too
|
||||||
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
|
if (hasEarpiece()) {
|
||||||
defaultAudioDevice = AudioDevice.EARPIECE;
|
defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
|
||||||
} else {
|
} else {
|
||||||
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
|
defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
|
||||||
}
|
}
|
||||||
// Create and initialize the proximity sensor.
|
// Create and initialize the proximity sensor.
|
||||||
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
|
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
|
||||||
|
@ -114,20 +113,13 @@ public class AppRTCAudioManager {
|
||||||
public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
|
public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
|
||||||
this.speakerPhonePreference = speakerPhonePreference;
|
this.speakerPhonePreference = speakerPhonePreference;
|
||||||
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
|
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
|
||||||
defaultAudioDevice = AudioDevice.EARPIECE;
|
defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE;
|
||||||
} else {
|
} else {
|
||||||
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
|
defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
|
||||||
}
|
}
|
||||||
updateAudioDeviceState();
|
updateAudioDeviceState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Construction.
|
|
||||||
*/
|
|
||||||
public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
|
|
||||||
return new AppRTCAudioManager(context, speakerPhonePreference);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isMicrophoneAvailable() {
|
public static boolean isMicrophoneAvailable() {
|
||||||
microphoneLatch = new CountDownLatch(1);
|
microphoneLatch = new CountDownLatch(1);
|
||||||
AudioRecord audioRecord = null;
|
AudioRecord audioRecord = null;
|
||||||
|
@ -174,16 +166,16 @@ public class AppRTCAudioManager {
|
||||||
}
|
}
|
||||||
// The proximity sensor should only be activated when there are exactly two
|
// The proximity sensor should only be activated when there are exactly two
|
||||||
// available audio devices.
|
// available audio devices.
|
||||||
if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
|
if (audioDevices.size() == 2 && audioDevices.contains(CallIntegration.AudioDevice.EARPIECE)
|
||||||
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
|
&& audioDevices.contains(CallIntegration.AudioDevice.SPEAKER_PHONE)) {
|
||||||
if (proximitySensor.sensorReportsNearState()) {
|
if (proximitySensor.sensorReportsNearState()) {
|
||||||
// Sensor reports that a "handset is being held up to a person's ear",
|
// Sensor reports that a "handset is being held up to a person's ear",
|
||||||
// or "something is covering the light sensor".
|
// or "something is covering the light sensor".
|
||||||
setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE);
|
setAudioDeviceInternal(CallIntegration.AudioDevice.EARPIECE);
|
||||||
} else {
|
} else {
|
||||||
// Sensor reports that a "handset is removed from a person's ear", or
|
// Sensor reports that a "handset is removed from a person's ear", or
|
||||||
// "the light sensor is no longer covered".
|
// "the light sensor is no longer covered".
|
||||||
setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
|
setAudioDeviceInternal(CallIntegration.AudioDevice.SPEAKER_PHONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,8 +250,8 @@ public class AppRTCAudioManager {
|
||||||
// Always disable microphone mute during a WebRTC call.
|
// Always disable microphone mute during a WebRTC call.
|
||||||
setMicrophoneMute(false);
|
setMicrophoneMute(false);
|
||||||
// Set initial device states.
|
// Set initial device states.
|
||||||
userSelectedAudioDevice = AudioDevice.NONE;
|
userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
|
||||||
selectedAudioDevice = AudioDevice.NONE;
|
selectedAudioDevice = CallIntegration.AudioDevice.NONE;
|
||||||
audioDevices.clear();
|
audioDevices.clear();
|
||||||
// Initialize and start Bluetooth if a BT device is available or initiate
|
// Initialize and start Bluetooth if a BT device is available or initiate
|
||||||
// detection of new (enabled) BT devices.
|
// detection of new (enabled) BT devices.
|
||||||
|
@ -315,7 +307,7 @@ public class AppRTCAudioManager {
|
||||||
/**
|
/**
|
||||||
* Changes selection of the currently active audio device.
|
* Changes selection of the currently active audio device.
|
||||||
*/
|
*/
|
||||||
private void setAudioDeviceInternal(AudioDevice device) {
|
private void setAudioDeviceInternal(CallIntegration.AudioDevice device) {
|
||||||
Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
|
Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
|
||||||
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
||||||
switch (device) {
|
switch (device) {
|
||||||
|
@ -338,7 +330,7 @@ public class AppRTCAudioManager {
|
||||||
* Changes default audio device.
|
* Changes default audio device.
|
||||||
* TODO(henrika): add usage of this method in the AppRTCMobile client.
|
* TODO(henrika): add usage of this method in the AppRTCMobile client.
|
||||||
*/
|
*/
|
||||||
public void setDefaultAudioDevice(AudioDevice defaultDevice) {
|
public void setDefaultAudioDevice(CallIntegration.AudioDevice defaultDevice) {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
switch (defaultDevice) {
|
switch (defaultDevice) {
|
||||||
case SPEAKER_PHONE:
|
case SPEAKER_PHONE:
|
||||||
|
@ -348,7 +340,7 @@ public class AppRTCAudioManager {
|
||||||
if (hasEarpiece()) {
|
if (hasEarpiece()) {
|
||||||
defaultAudioDevice = defaultDevice;
|
defaultAudioDevice = defaultDevice;
|
||||||
} else {
|
} else {
|
||||||
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
|
defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -362,7 +354,7 @@ public class AppRTCAudioManager {
|
||||||
/**
|
/**
|
||||||
* Changes selection of the currently active audio device.
|
* Changes selection of the currently active audio device.
|
||||||
*/
|
*/
|
||||||
public void selectAudioDevice(AudioDevice device) {
|
public void selectAudioDevice(CallIntegration.AudioDevice device) {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
if (!audioDevices.contains(device)) {
|
if (!audioDevices.contains(device)) {
|
||||||
Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices);
|
Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices);
|
||||||
|
@ -374,7 +366,7 @@ public class AppRTCAudioManager {
|
||||||
/**
|
/**
|
||||||
* Returns current set of available/selectable audio devices.
|
* Returns current set of available/selectable audio devices.
|
||||||
*/
|
*/
|
||||||
public Set<AudioDevice> getAudioDevices() {
|
public Set<CallIntegration.AudioDevice> getAudioDevices() {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
return Collections.unmodifiableSet(new HashSet<>(audioDevices));
|
return Collections.unmodifiableSet(new HashSet<>(audioDevices));
|
||||||
}
|
}
|
||||||
|
@ -382,7 +374,7 @@ public class AppRTCAudioManager {
|
||||||
/**
|
/**
|
||||||
* Returns the currently selected audio device.
|
* Returns the currently selected audio device.
|
||||||
*/
|
*/
|
||||||
public AudioDevice getSelectedAudioDevice() {
|
public CallIntegration.AudioDevice getSelectedAudioDevice() {
|
||||||
ThreadUtils.checkIsOnMainThread();
|
ThreadUtils.checkIsOnMainThread();
|
||||||
return selectedAudioDevice;
|
return selectedAudioDevice;
|
||||||
}
|
}
|
||||||
|
@ -479,21 +471,21 @@ public class AppRTCAudioManager {
|
||||||
bluetoothManager.updateDevice();
|
bluetoothManager.updateDevice();
|
||||||
}
|
}
|
||||||
// Update the set of available audio devices.
|
// Update the set of available audio devices.
|
||||||
Set<AudioDevice> newAudioDevices = new HashSet<>();
|
Set<CallIntegration.AudioDevice> newAudioDevices = new HashSet<>();
|
||||||
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
||||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
||||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
|
||||||
newAudioDevices.add(AudioDevice.BLUETOOTH);
|
newAudioDevices.add(CallIntegration.AudioDevice.BLUETOOTH);
|
||||||
}
|
}
|
||||||
if (hasWiredHeadset) {
|
if (hasWiredHeadset) {
|
||||||
// If a wired headset is connected, then it is the only possible option.
|
// If a wired headset is connected, then it is the only possible option.
|
||||||
newAudioDevices.add(AudioDevice.WIRED_HEADSET);
|
newAudioDevices.add(CallIntegration.AudioDevice.WIRED_HEADSET);
|
||||||
} else {
|
} else {
|
||||||
// No wired headset, hence the audio-device list can contain speaker
|
// No wired headset, hence the audio-device list can contain speaker
|
||||||
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
||||||
newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
|
newAudioDevices.add(CallIntegration.AudioDevice.SPEAKER_PHONE);
|
||||||
if (hasEarpiece()) {
|
if (hasEarpiece()) {
|
||||||
newAudioDevices.add(AudioDevice.EARPIECE);
|
newAudioDevices.add(CallIntegration.AudioDevice.EARPIECE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Store state which is set to true if the device list has changed.
|
// Store state which is set to true if the device list has changed.
|
||||||
|
@ -502,33 +494,33 @@ public class AppRTCAudioManager {
|
||||||
audioDevices = newAudioDevices;
|
audioDevices = newAudioDevices;
|
||||||
// Correct user selected audio devices if needed.
|
// Correct user selected audio devices if needed.
|
||||||
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
|
||||||
&& userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
|
&& userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH) {
|
||||||
// If BT is not available, it can't be the user selection.
|
// If BT is not available, it can't be the user selection.
|
||||||
userSelectedAudioDevice = AudioDevice.NONE;
|
userSelectedAudioDevice = CallIntegration.AudioDevice.NONE;
|
||||||
}
|
}
|
||||||
if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) {
|
if (hasWiredHeadset && userSelectedAudioDevice == CallIntegration.AudioDevice.SPEAKER_PHONE) {
|
||||||
// If user selected speaker phone, but then plugged wired headset then make
|
// If user selected speaker phone, but then plugged wired headset then make
|
||||||
// wired headset as user selected device.
|
// wired headset as user selected device.
|
||||||
userSelectedAudioDevice = AudioDevice.WIRED_HEADSET;
|
userSelectedAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
|
||||||
}
|
}
|
||||||
if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) {
|
if (!hasWiredHeadset && userSelectedAudioDevice == CallIntegration.AudioDevice.WIRED_HEADSET) {
|
||||||
// If user selected wired headset, but then unplugged wired headset then make
|
// If user selected wired headset, but then unplugged wired headset then make
|
||||||
// speaker phone as user selected device.
|
// speaker phone as user selected device.
|
||||||
userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE;
|
userSelectedAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE;
|
||||||
}
|
}
|
||||||
// Need to start Bluetooth if it is available and user either selected it explicitly or
|
// Need to start Bluetooth if it is available and user either selected it explicitly or
|
||||||
// user did not select any output device.
|
// user did not select any output device.
|
||||||
boolean needBluetoothAudioStart =
|
boolean needBluetoothAudioStart =
|
||||||
bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
||||||
&& (userSelectedAudioDevice == AudioDevice.NONE
|
&& (userSelectedAudioDevice == CallIntegration.AudioDevice.NONE
|
||||||
|| userSelectedAudioDevice == AudioDevice.BLUETOOTH);
|
|| userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH);
|
||||||
// Need to stop Bluetooth audio if user selected different device and
|
// Need to stop Bluetooth audio if user selected different device and
|
||||||
// Bluetooth SCO connection is established or in the process.
|
// Bluetooth SCO connection is established or in the process.
|
||||||
boolean needBluetoothAudioStop =
|
boolean needBluetoothAudioStop =
|
||||||
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
||||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
|
||||||
&& (userSelectedAudioDevice != AudioDevice.NONE
|
&& (userSelectedAudioDevice != CallIntegration.AudioDevice.NONE
|
||||||
&& userSelectedAudioDevice != AudioDevice.BLUETOOTH);
|
&& userSelectedAudioDevice != CallIntegration.AudioDevice.BLUETOOTH);
|
||||||
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
||||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
||||||
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
||||||
|
@ -545,21 +537,21 @@ public class AppRTCAudioManager {
|
||||||
// Attempt to start Bluetooth SCO audio (takes a few second to start).
|
// Attempt to start Bluetooth SCO audio (takes a few second to start).
|
||||||
if (!bluetoothManager.startScoAudio()) {
|
if (!bluetoothManager.startScoAudio()) {
|
||||||
// Remove BLUETOOTH from list of available devices since SCO failed.
|
// Remove BLUETOOTH from list of available devices since SCO failed.
|
||||||
audioDevices.remove(AudioDevice.BLUETOOTH);
|
audioDevices.remove(CallIntegration.AudioDevice.BLUETOOTH);
|
||||||
audioDeviceSetUpdated = true;
|
audioDeviceSetUpdated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update selected audio device.
|
// Update selected audio device.
|
||||||
final AudioDevice newAudioDevice;
|
final CallIntegration.AudioDevice newAudioDevice;
|
||||||
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
||||||
// If a Bluetooth is connected, then it should be used as output audio
|
// If a Bluetooth is connected, then it should be used as output audio
|
||||||
// device. Note that it is not sufficient that a headset is available;
|
// device. Note that it is not sufficient that a headset is available;
|
||||||
// an active SCO channel must also be up and running.
|
// an active SCO channel must also be up and running.
|
||||||
newAudioDevice = AudioDevice.BLUETOOTH;
|
newAudioDevice = CallIntegration.AudioDevice.BLUETOOTH;
|
||||||
} else if (hasWiredHeadset) {
|
} else if (hasWiredHeadset) {
|
||||||
// If a wired headset is connected, but Bluetooth is not, then wired headset is used as
|
// If a wired headset is connected, but Bluetooth is not, then wired headset is used as
|
||||||
// audio device.
|
// audio device.
|
||||||
newAudioDevice = AudioDevice.WIRED_HEADSET;
|
newAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET;
|
||||||
} else {
|
} else {
|
||||||
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker
|
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker
|
||||||
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
||||||
|
@ -582,12 +574,6 @@ public class AppRTCAudioManager {
|
||||||
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
|
Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* AudioDevice is the names of possible audio devices that we currently
|
|
||||||
* support.
|
|
||||||
*/
|
|
||||||
public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AudioManager state.
|
* AudioManager state.
|
||||||
*/
|
*/
|
||||||
|
@ -615,7 +601,7 @@ public class AppRTCAudioManager {
|
||||||
public interface AudioManagerEvents {
|
public interface AudioManagerEvents {
|
||||||
// Callback fired once audio device is changed or list of available audio devices changed.
|
// Callback fired once audio device is changed or list of available audio devices changed.
|
||||||
void onAudioDeviceChanged(
|
void onAudioDeviceChanged(
|
||||||
AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
|
CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Receiver which handles changes in wired headset availability. */
|
/* Receiver which handles changes in wired headset availability. */
|
||||||
|
|
|
@ -0,0 +1,408 @@
|
||||||
|
package eu.siacs.conversations.services;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.telecom.CallAudioState;
|
||||||
|
import android.telecom.CallEndpoint;
|
||||||
|
import android.telecom.Connection;
|
||||||
|
import android.telecom.DisconnectCause;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.ui.util.MainThreadExecutor;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class CallIntegration extends Connection {
|
||||||
|
|
||||||
|
private final AppRTCAudioManager appRTCAudioManager;
|
||||||
|
private AudioDevice initialAudioDevice = null;
|
||||||
|
private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private List<CallEndpoint> availableEndpoints = Collections.emptyList();
|
||||||
|
|
||||||
|
private Callback callback = null;
|
||||||
|
|
||||||
|
public CallIntegration(final Context context) {
|
||||||
|
if (selfManaged()) {
|
||||||
|
setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
|
||||||
|
this.appRTCAudioManager = null;
|
||||||
|
} else {
|
||||||
|
this.appRTCAudioManager = new AppRTCAudioManager(context);
|
||||||
|
this.appRTCAudioManager.start(this::onAudioDeviceChanged);
|
||||||
|
// TODO WebRTCWrapper would issue one call to eventCallback.onAudioDeviceChanged
|
||||||
|
}
|
||||||
|
setRingbackRequested(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCallback(final Callback callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShowIncomingCallUi() {
|
||||||
|
Log.d(Config.LOGTAG, "onShowIncomingCallUi");
|
||||||
|
this.callback.onCallIntegrationShowIncomingCallUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnswer() {
|
||||||
|
Log.d(Config.LOGTAG, "onAnswer()");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnect() {
|
||||||
|
Log.d(Config.LOGTAG, "onDisconnect()");
|
||||||
|
this.callback.onCallIntegrationDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReject() {
|
||||||
|
Log.d(Config.LOGTAG, "onReject()");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReject(final String replyMessage) {
|
||||||
|
Log.d(Config.LOGTAG, "onReject(" + replyMessage + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
@Override
|
||||||
|
public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {
|
||||||
|
Log.d(Config.LOGTAG, "onAvailableCallEndpointsChanged(" + availableEndpoints + ")");
|
||||||
|
this.availableEndpoints = availableEndpoints;
|
||||||
|
this.onAudioDeviceChanged(
|
||||||
|
getAudioDeviceUpsideDownCake(getCurrentCallEndpoint()),
|
||||||
|
ImmutableSet.copyOf(
|
||||||
|
Lists.transform(
|
||||||
|
availableEndpoints,
|
||||||
|
CallIntegration::getAudioDeviceUpsideDownCake)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
@Override
|
||||||
|
public void onCallEndpointChanged(@NonNull final CallEndpoint callEndpoint) {
|
||||||
|
Log.d(Config.LOGTAG, "onCallEndpointChanged()");
|
||||||
|
this.onAudioDeviceChanged(
|
||||||
|
getAudioDeviceUpsideDownCake(callEndpoint),
|
||||||
|
ImmutableSet.copyOf(
|
||||||
|
Lists.transform(
|
||||||
|
this.availableEndpoints,
|
||||||
|
CallIntegration::getAudioDeviceUpsideDownCake)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCallAudioStateChanged(final CallAudioState state) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
Log.d(Config.LOGTAG, "ignoring onCallAudioStateChange() on Upside Down Cake");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(Config.LOGTAG, "onCallAudioStateChange(" + state + ")");
|
||||||
|
this.onAudioDeviceChanged(getAudioDeviceOreo(state), getAudioDevicesOreo(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<AudioDevice> getAudioDevices() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
return getAudioDevicesUpsideDownCake();
|
||||||
|
} else if (selfManaged()) {
|
||||||
|
return getAudioDevicesOreo();
|
||||||
|
} else {
|
||||||
|
return getAudioDevicesFallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioDevice getSelectedAudioDevice() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
return getAudioDeviceUpsideDownCake();
|
||||||
|
} else if (selfManaged()) {
|
||||||
|
return getAudioDeviceOreo();
|
||||||
|
} else {
|
||||||
|
return getAudioDeviceFallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioDevice(final AudioDevice audioDevice) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
setAudioDeviceUpsideDownCake(audioDevice);
|
||||||
|
} else if (selfManaged()) {
|
||||||
|
setAudioDeviceOreo(audioDevice);
|
||||||
|
} else {
|
||||||
|
setAudioDeviceFallback(audioDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
private Set<AudioDevice> getAudioDevicesUpsideDownCake() {
|
||||||
|
return ImmutableSet.copyOf(
|
||||||
|
Lists.transform(
|
||||||
|
this.availableEndpoints, CallIntegration::getAudioDeviceUpsideDownCake));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
private AudioDevice getAudioDeviceUpsideDownCake() {
|
||||||
|
return getAudioDeviceUpsideDownCake(getCurrentCallEndpoint());
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
private static AudioDevice getAudioDeviceUpsideDownCake(final CallEndpoint callEndpoint) {
|
||||||
|
if (callEndpoint == null) {
|
||||||
|
return AudioDevice.NONE;
|
||||||
|
}
|
||||||
|
final var endpointType = callEndpoint.getEndpointType();
|
||||||
|
return switch (endpointType) {
|
||||||
|
case CallEndpoint.TYPE_BLUETOOTH -> AudioDevice.BLUETOOTH;
|
||||||
|
case CallEndpoint.TYPE_EARPIECE -> AudioDevice.EARPIECE;
|
||||||
|
case CallEndpoint.TYPE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
|
||||||
|
case CallEndpoint.TYPE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
|
||||||
|
case CallEndpoint.TYPE_STREAMING -> AudioDevice.STREAMING;
|
||||||
|
case CallEndpoint.TYPE_UNKNOWN -> AudioDevice.NONE;
|
||||||
|
default -> throw new IllegalStateException("Unknown endpoint type " + endpointType);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
private void setAudioDeviceUpsideDownCake(final AudioDevice audioDevice) {
|
||||||
|
final var callEndpointOptional =
|
||||||
|
Iterables.tryFind(
|
||||||
|
this.availableEndpoints,
|
||||||
|
e -> getAudioDeviceUpsideDownCake(e) == audioDevice);
|
||||||
|
if (callEndpointOptional.isPresent()) {
|
||||||
|
final var endpoint = callEndpointOptional.get();
|
||||||
|
requestCallEndpointChange(
|
||||||
|
endpoint,
|
||||||
|
MainThreadExecutor.getInstance(),
|
||||||
|
result -> Log.d(Config.LOGTAG, "switched to endpoint " + endpoint));
|
||||||
|
} else {
|
||||||
|
Log.w(Config.LOGTAG, "no endpoint found matching " + audioDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<AudioDevice> getAudioDevicesOreo() {
|
||||||
|
final var audioState = getCallAudioState();
|
||||||
|
if (audioState == null) {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"no CallAudioState available. returning empty set for audio devices");
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
return getAudioDevicesOreo(audioState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<AudioDevice> getAudioDevicesOreo(final CallAudioState callAudioState) {
|
||||||
|
final ImmutableSet.Builder<AudioDevice> supportedAudioDevicesBuilder =
|
||||||
|
new ImmutableSet.Builder<>();
|
||||||
|
final var supportedRouteMask = callAudioState.getSupportedRouteMask();
|
||||||
|
if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH)
|
||||||
|
== CallAudioState.ROUTE_BLUETOOTH) {
|
||||||
|
supportedAudioDevicesBuilder.add(AudioDevice.BLUETOOTH);
|
||||||
|
}
|
||||||
|
if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) {
|
||||||
|
supportedAudioDevicesBuilder.add(AudioDevice.EARPIECE);
|
||||||
|
}
|
||||||
|
if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) {
|
||||||
|
supportedAudioDevicesBuilder.add(AudioDevice.SPEAKER_PHONE);
|
||||||
|
}
|
||||||
|
if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET)
|
||||||
|
== CallAudioState.ROUTE_WIRED_HEADSET) {
|
||||||
|
supportedAudioDevicesBuilder.add(AudioDevice.WIRED_HEADSET);
|
||||||
|
}
|
||||||
|
return supportedAudioDevicesBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AudioDevice getAudioDeviceOreo() {
|
||||||
|
final var audioState = getCallAudioState();
|
||||||
|
if (audioState == null) {
|
||||||
|
Log.d(Config.LOGTAG, "no CallAudioState available. returning NONE as audio device");
|
||||||
|
return AudioDevice.NONE;
|
||||||
|
}
|
||||||
|
return getAudioDeviceOreo(audioState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AudioDevice getAudioDeviceOreo(final CallAudioState audioState) {
|
||||||
|
// technically we get a mask here; maybe we should query the mask instead
|
||||||
|
return switch (audioState.getRoute()) {
|
||||||
|
case CallAudioState.ROUTE_BLUETOOTH -> AudioDevice.BLUETOOTH;
|
||||||
|
case CallAudioState.ROUTE_EARPIECE -> AudioDevice.EARPIECE;
|
||||||
|
case CallAudioState.ROUTE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
|
||||||
|
case CallAudioState.ROUTE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
|
||||||
|
default -> AudioDevice.NONE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
private void setAudioDeviceOreo(final AudioDevice audioDevice) {
|
||||||
|
switch (audioDevice) {
|
||||||
|
case EARPIECE -> setAudioRoute(CallAudioState.ROUTE_EARPIECE);
|
||||||
|
case BLUETOOTH -> setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
|
||||||
|
case WIRED_HEADSET -> setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
|
||||||
|
case SPEAKER_PHONE -> setAudioRoute(CallAudioState.ROUTE_SPEAKER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<AudioDevice> getAudioDevicesFallback() {
|
||||||
|
return requireAppRtcAudioManager().getAudioDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AudioDevice getAudioDeviceFallback() {
|
||||||
|
return requireAppRtcAudioManager().getSelectedAudioDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAudioDeviceFallback(final AudioDevice audioDevice) {
|
||||||
|
requireAppRtcAudioManager().setDefaultAudioDevice(audioDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private AppRTCAudioManager requireAppRtcAudioManager() {
|
||||||
|
if (this.appRTCAudioManager == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"You are trying to access the fallback audio manager on a modern device");
|
||||||
|
}
|
||||||
|
return this.appRTCAudioManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStateChanged(final int state) {
|
||||||
|
Log.d(Config.LOGTAG, "onStateChanged(" + state + ")");
|
||||||
|
if (state == STATE_DISCONNECTED) {
|
||||||
|
final var audioManager = this.appRTCAudioManager;
|
||||||
|
if (audioManager != null) {
|
||||||
|
audioManager.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void success() {
|
||||||
|
Log.d(Config.LOGTAG, "CallIntegration.success()");
|
||||||
|
this.destroyWith(new DisconnectCause(DisconnectCause.LOCAL, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void accepted() {
|
||||||
|
Log.d(Config.LOGTAG, "CallIntegration.accepted()");
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
this.destroyWith(new DisconnectCause(DisconnectCause.ANSWERED_ELSEWHERE, null));
|
||||||
|
} else {
|
||||||
|
this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void error() {
|
||||||
|
Log.d(Config.LOGTAG, "CallIntegration.error()");
|
||||||
|
this.destroyWith(new DisconnectCause(DisconnectCause.ERROR, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void retracted() {
|
||||||
|
Log.d(Config.LOGTAG, "CallIntegration.retracted()");
|
||||||
|
// an alternative cause would be LOCAL
|
||||||
|
this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rejected() {
|
||||||
|
Log.d(Config.LOGTAG, "CallIntegration.rejected()");
|
||||||
|
this.destroyWith(new DisconnectCause(DisconnectCause.REJECTED, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void busy() {
|
||||||
|
Log.d(Config.LOGTAG, "CallIntegration.busy()");
|
||||||
|
this.destroyWith(new DisconnectCause(DisconnectCause.BUSY, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void destroyWith(final DisconnectCause disconnectCause) {
|
||||||
|
if (this.getState() == STATE_DISCONNECTED) {
|
||||||
|
Log.d(Config.LOGTAG, "CallIntegration has already been destroyed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setDisconnected(disconnectCause);
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri address(final Jid contact) {
|
||||||
|
return Uri.parse(String.format("xmpp:%s", contact.toEscapedString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyDisconnected() {
|
||||||
|
if (this.getState() == STATE_DISCONNECTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new AssertionError("CallIntegration has not been disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onAudioDeviceChanged(
|
||||||
|
final CallIntegration.AudioDevice selectedAudioDevice,
|
||||||
|
final Set<CallIntegration.AudioDevice> availableAudioDevices) {
|
||||||
|
if (this.initialAudioDevice != null
|
||||||
|
&& this.initialAudioDeviceConfigured.compareAndSet(false, true)) {
|
||||||
|
if (availableAudioDevices.contains(this.initialAudioDevice)) {
|
||||||
|
setAudioDevice(this.initialAudioDevice);
|
||||||
|
Log.d(Config.LOGTAG, "configured initial audio device");
|
||||||
|
} else {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"initial audio device not available. available devices: "
|
||||||
|
+ availableAudioDevices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final var callback = this.callback;
|
||||||
|
if (callback == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean selfManaged() {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInitialAudioDevice(final AudioDevice audioDevice) {
|
||||||
|
Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")");
|
||||||
|
this.initialAudioDevice = audioDevice;
|
||||||
|
if (CallIntegration.selfManaged()) {
|
||||||
|
// once the 'CallIntegration' gets added to the system we receive calls to update audio
|
||||||
|
// state
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var audioManager = requireAppRtcAudioManager();
|
||||||
|
this.onAudioDeviceChanged(
|
||||||
|
audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AudioDevice is the names of possible audio devices that we currently support. */
|
||||||
|
public enum AudioDevice {
|
||||||
|
NONE,
|
||||||
|
SPEAKER_PHONE,
|
||||||
|
WIRED_HEADSET,
|
||||||
|
EARPIECE,
|
||||||
|
BLUETOOTH,
|
||||||
|
STREAMING
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AudioDevice initialAudioDevice(final Set<Media> media) {
|
||||||
|
if (Media.audioOnly(media)) {
|
||||||
|
return AudioDevice.EARPIECE;
|
||||||
|
} else {
|
||||||
|
return AudioDevice.SPEAKER_PHONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void onCallIntegrationShowIncomingCallUi();
|
||||||
|
|
||||||
|
void onCallIntegrationDisconnect();
|
||||||
|
|
||||||
|
void onAudioDeviceChanged(
|
||||||
|
CallIntegration.AudioDevice selectedAudioDevice,
|
||||||
|
Set<CallIntegration.AudioDevice> availableAudioDevices);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,255 @@
|
||||||
|
package eu.siacs.conversations.services;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.telecom.Connection;
|
||||||
|
import android.telecom.ConnectionRequest;
|
||||||
|
import android.telecom.ConnectionService;
|
||||||
|
import android.telecom.DisconnectCause;
|
||||||
|
import android.telecom.PhoneAccount;
|
||||||
|
import android.telecom.PhoneAccountHandle;
|
||||||
|
import android.telecom.TelecomManager;
|
||||||
|
import android.telecom.VideoProfile;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
import eu.siacs.conversations.ui.RtpSessionActivity;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
public class CallIntegrationConnectionService extends ConnectionService {
|
||||||
|
|
||||||
|
private ListenableFuture<ServiceConnectionService> serviceFuture;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
this.serviceFuture = ServiceConnectionService.bindService(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
Log.d(Config.LOGTAG, "destroying CallIntegrationConnectionService");
|
||||||
|
super.onDestroy();
|
||||||
|
final ServiceConnection serviceConnection;
|
||||||
|
try {
|
||||||
|
serviceConnection = serviceFuture.get().serviceConnection;
|
||||||
|
} catch (final Exception e) {
|
||||||
|
Log.d(Config.LOGTAG, "could not fetch service connection", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.unbindService(serviceConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Connection onCreateOutgoingConnection(
|
||||||
|
final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
|
||||||
|
Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")");
|
||||||
|
final var uri = request.getAddress();
|
||||||
|
final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
|
||||||
|
final var extras = request.getExtras();
|
||||||
|
final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE);
|
||||||
|
final Set<Media> media =
|
||||||
|
videoState == VideoProfile.STATE_AUDIO_ONLY
|
||||||
|
? ImmutableSet.of(Media.AUDIO)
|
||||||
|
: ImmutableSet.of(Media.AUDIO, Media.VIDEO);
|
||||||
|
Log.d(Config.LOGTAG, "jid=" + jid);
|
||||||
|
Log.d(Config.LOGTAG, "phoneAccountHandle:" + phoneAccountHandle.getId());
|
||||||
|
Log.d(Config.LOGTAG, "media " + media);
|
||||||
|
final var service = ServiceConnectionService.get(this.serviceFuture);
|
||||||
|
if (service == null) {
|
||||||
|
return Connection.createFailedConnection(
|
||||||
|
new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
|
||||||
|
}
|
||||||
|
final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
|
||||||
|
final Intent intent = new Intent(this, RtpSessionActivity.class);
|
||||||
|
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
|
||||||
|
intent.putExtra(RtpSessionActivity.EXTRA_WITH, jid.toEscapedString());
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
|
final CallIntegration callIntegration;
|
||||||
|
if (jid.isBareJid()) {
|
||||||
|
final var proposal =
|
||||||
|
service.getJingleConnectionManager()
|
||||||
|
.proposeJingleRtpSession(account, jid, media);
|
||||||
|
|
||||||
|
if (Media.audioOnly(media)) {
|
||||||
|
intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
|
||||||
|
} else {
|
||||||
|
intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
|
||||||
|
}
|
||||||
|
callIntegration = proposal.getCallIntegration();
|
||||||
|
} else {
|
||||||
|
final JingleRtpConnection jingleRtpConnection =
|
||||||
|
service.getJingleConnectionManager().initializeRtpSession(account, jid, media);
|
||||||
|
final String sessionId = jingleRtpConnection.getId().sessionId;
|
||||||
|
intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
|
||||||
|
callIntegration = jingleRtpConnection.getCallIntegration();
|
||||||
|
}
|
||||||
|
Log.d(Config.LOGTAG, "start activity!");
|
||||||
|
startActivity(intent);
|
||||||
|
return callIntegration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection onCreateIncomingConnection(
|
||||||
|
final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
|
||||||
|
final var service = ServiceConnectionService.get(this.serviceFuture);
|
||||||
|
final Bundle extras = request.getExtras();
|
||||||
|
final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
|
||||||
|
final String incomingCallAddress =
|
||||||
|
extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
|
||||||
|
final String sid = extraExtras == null ? null : extraExtras.getString("sid");
|
||||||
|
Log.d(Config.LOGTAG, "sid " + sid);
|
||||||
|
final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress);
|
||||||
|
Log.d(Config.LOGTAG, "uri=" + uri);
|
||||||
|
if (uri == null || sid == null) {
|
||||||
|
return Connection.createFailedConnection(
|
||||||
|
new DisconnectCause(
|
||||||
|
DisconnectCause.ERROR,
|
||||||
|
"connection request is missing required information"));
|
||||||
|
}
|
||||||
|
if (service == null) {
|
||||||
|
return Connection.createFailedConnection(
|
||||||
|
new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
|
||||||
|
}
|
||||||
|
final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
|
||||||
|
final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
|
||||||
|
final var weakReference =
|
||||||
|
service.getJingleConnectionManager().findJingleRtpConnection(account, jid, sid);
|
||||||
|
if (weakReference == null) {
|
||||||
|
Log.d(Config.LOGTAG, "no connection found for " + jid + " and sid=" + sid);
|
||||||
|
return Connection.createFailedConnection(
|
||||||
|
new DisconnectCause(DisconnectCause.ERROR, "no incoming connection found"));
|
||||||
|
}
|
||||||
|
final var jingleRtpConnection = weakReference.get();
|
||||||
|
if (jingleRtpConnection == null) {
|
||||||
|
Log.d(Config.LOGTAG, "connection has been terminated");
|
||||||
|
return Connection.createFailedConnection(
|
||||||
|
new DisconnectCause(DisconnectCause.ERROR, "connection has been terminated"));
|
||||||
|
}
|
||||||
|
Log.d(Config.LOGTAG, "registering call integration for incoming call");
|
||||||
|
return jingleRtpConnection.getCallIntegration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerPhoneAccount(final Context context, final Account account) {
|
||||||
|
final var builder =
|
||||||
|
PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid());
|
||||||
|
builder.setSupportedUriSchemes(Collections.singletonList("xmpp"));
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
builder.setCapabilities(
|
||||||
|
PhoneAccount.CAPABILITY_SELF_MANAGED
|
||||||
|
| PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
|
||||||
|
}
|
||||||
|
final var phoneAccount = builder.build();
|
||||||
|
|
||||||
|
context.getSystemService(TelecomManager.class).registerPhoneAccount(phoneAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerPhoneAccounts(
|
||||||
|
final Context context, final Collection<Account> accounts) {
|
||||||
|
for (final Account account : accounts) {
|
||||||
|
registerPhoneAccount(context, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PhoneAccountHandle getHandle(final Context context, final Account account) {
|
||||||
|
final var competentName =
|
||||||
|
new ComponentName(context, CallIntegrationConnectionService.class);
|
||||||
|
return new PhoneAccountHandle(competentName, account.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void placeCall(
|
||||||
|
final Context context, final Account account, final Jid with, final Set<Media> media) {
|
||||||
|
Log.d(Config.LOGTAG, "place call media=" + media);
|
||||||
|
final var extras = new Bundle();
|
||||||
|
extras.putParcelable(
|
||||||
|
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(context, account));
|
||||||
|
extras.putInt(
|
||||||
|
TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
|
||||||
|
Media.audioOnly(media)
|
||||||
|
? VideoProfile.STATE_AUDIO_ONLY
|
||||||
|
: VideoProfile.STATE_BIDIRECTIONAL);
|
||||||
|
context.getSystemService(TelecomManager.class)
|
||||||
|
.placeCall(CallIntegration.address(with), extras);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addNewIncomingCall(
|
||||||
|
final Context context, final AbstractJingleConnection.Id id) {
|
||||||
|
final var phoneAccountHandle =
|
||||||
|
CallIntegrationConnectionService.getHandle(context, id.account);
|
||||||
|
final var bundle = new Bundle();
|
||||||
|
bundle.putString(
|
||||||
|
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
|
||||||
|
CallIntegration.address(id.with).toString());
|
||||||
|
final var extras = new Bundle();
|
||||||
|
extras.putString("sid", id.sessionId);
|
||||||
|
bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
|
||||||
|
context.getSystemService(TelecomManager.class)
|
||||||
|
.addNewIncomingCall(phoneAccountHandle, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ServiceConnectionService {
|
||||||
|
private final ServiceConnection serviceConnection;
|
||||||
|
private final XmppConnectionService service;
|
||||||
|
|
||||||
|
public ServiceConnectionService(
|
||||||
|
final ServiceConnection serviceConnection, final XmppConnectionService service) {
|
||||||
|
this.serviceConnection = serviceConnection;
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static XmppConnectionService get(
|
||||||
|
final ListenableFuture<ServiceConnectionService> future) {
|
||||||
|
try {
|
||||||
|
return future.get(2, TimeUnit.SECONDS).service;
|
||||||
|
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ListenableFuture<ServiceConnectionService> bindService(
|
||||||
|
final Context context) {
|
||||||
|
final SettableFuture<ServiceConnectionService> serviceConnectionFuture =
|
||||||
|
SettableFuture.create();
|
||||||
|
final var intent = new Intent(context, XmppConnectionService.class);
|
||||||
|
intent.setAction(XmppConnectionService.ACTION_CALL_INTEGRATION_SERVICE_STARTED);
|
||||||
|
final var serviceConnection =
|
||||||
|
new ServiceConnection() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(
|
||||||
|
final ComponentName name, final IBinder iBinder) {
|
||||||
|
final XmppConnectionService.XmppConnectionBinder binder =
|
||||||
|
(XmppConnectionService.XmppConnectionBinder) iBinder;
|
||||||
|
serviceConnectionFuture.set(
|
||||||
|
new ServiceConnectionService(this, binder.getService()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(final ComponentName name) {}
|
||||||
|
};
|
||||||
|
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
return serviceConnectionFuture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -198,6 +198,7 @@ public class XmppConnectionService extends Service {
|
||||||
public static final String ACTION_DISMISS_CALL = "dismiss_call";
|
public static final String ACTION_DISMISS_CALL = "dismiss_call";
|
||||||
public static final String ACTION_END_CALL = "end_call";
|
public static final String ACTION_END_CALL = "end_call";
|
||||||
public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
|
public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
|
||||||
|
public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED = "call_integration_service_started";
|
||||||
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
|
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
|
||||||
public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
|
public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
|
||||||
public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
|
public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
|
||||||
|
@ -303,16 +304,6 @@ public class XmppConnectionService extends Service {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false);
|
|
||||||
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
|
|
||||||
@Override
|
|
||||||
public void onCallStateChanged(final int state, final String phoneNumber) {
|
|
||||||
isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE);
|
|
||||||
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
|
|
||||||
mJingleConnectionManager.notifyPhoneCallStarted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private boolean destroyed = false;
|
private boolean destroyed = false;
|
||||||
|
|
||||||
|
@ -1288,6 +1279,8 @@ public class XmppConnectionService extends Service {
|
||||||
toggleSetProfilePictureActivity(hasEnabledAccounts);
|
toggleSetProfilePictureActivity(hasEnabledAccounts);
|
||||||
reconfigurePushDistributor();
|
reconfigurePushDistributor();
|
||||||
|
|
||||||
|
CallIntegrationConnectionService.registerPhoneAccounts(this, this.accounts);
|
||||||
|
|
||||||
restoreFromDatabase();
|
restoreFromDatabase();
|
||||||
|
|
||||||
if (QuickConversationsService.isContactListIntegration(this)
|
if (QuickConversationsService.isContactListIntegration(this)
|
||||||
|
@ -1351,23 +1344,10 @@ public class XmppConnectionService extends Service {
|
||||||
ContextCompat.RECEIVER_EXPORTED);
|
ContextCompat.RECEIVER_EXPORTED);
|
||||||
mForceDuringOnCreate.set(false);
|
mForceDuringOnCreate.set(false);
|
||||||
toggleForegroundService();
|
toggleForegroundService();
|
||||||
setupPhoneStateListener();
|
|
||||||
internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS);
|
internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void setupPhoneStateListener() {
|
|
||||||
final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
|
|
||||||
if (telephonyManager == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPhoneInCall() {
|
|
||||||
return isPhoneInCall.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkForDeletedFiles() {
|
private void checkForDeletedFiles() {
|
||||||
if (destroyed) {
|
if (destroyed) {
|
||||||
Log.d(Config.LOGTAG, "Do not check for deleted files because service has been destroyed");
|
Log.d(Config.LOGTAG, "Do not check for deleted files because service has been destroyed");
|
||||||
|
@ -4413,7 +4393,7 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyJingleRtpConnectionUpdate(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
|
public void notifyJingleRtpConnectionUpdate(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices) {
|
||||||
for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
|
for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
|
||||||
listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
|
listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
|
||||||
}
|
}
|
||||||
|
@ -5110,7 +5090,7 @@ public class XmppConnectionService extends Service {
|
||||||
public interface OnJingleRtpConnectionUpdate {
|
public interface OnJingleRtpConnectionUpdate {
|
||||||
void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state);
|
void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state);
|
||||||
|
|
||||||
void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
|
void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnAccountUpdate {
|
public interface OnAccountUpdate {
|
||||||
|
|
|
@ -86,6 +86,7 @@ import eu.siacs.conversations.entities.Transferable;
|
||||||
import eu.siacs.conversations.entities.TransferablePlaceholder;
|
import eu.siacs.conversations.entities.TransferablePlaceholder;
|
||||||
import eu.siacs.conversations.http.HttpDownloadConnection;
|
import eu.siacs.conversations.http.HttpDownloadConnection;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
|
import eu.siacs.conversations.services.CallIntegrationConnectionService;
|
||||||
import eu.siacs.conversations.services.MessageArchiveService;
|
import eu.siacs.conversations.services.MessageArchiveService;
|
||||||
import eu.siacs.conversations.services.QuickConversationsService;
|
import eu.siacs.conversations.services.QuickConversationsService;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
|
@ -1652,13 +1653,14 @@ public class ConversationFragment extends XmppFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void triggerRtpSession(final Account account, final Jid with, final String action) {
|
private void triggerRtpSession(final Account account, final Jid with, final String action) {
|
||||||
final Intent intent = new Intent(activity, RtpSessionActivity.class);
|
CallIntegrationConnectionService.placeCall(requireActivity(),account,with,RtpSessionActivity.actionToMedia(action));
|
||||||
|
/*final Intent intent = new Intent(activity, RtpSessionActivity.class);
|
||||||
intent.setAction(action);
|
intent.setAction(action);
|
||||||
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
|
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
|
||||||
intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
|
intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
startActivity(intent);
|
startActivity(intent);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAttachmentSelection(MenuItem item) {
|
private void handleAttachmentSelection(MenuItem item) {
|
||||||
|
|
|
@ -49,6 +49,8 @@ import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||||
|
import eu.siacs.conversations.services.CallIntegration;
|
||||||
|
import eu.siacs.conversations.services.CallIntegrationConnectionService;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||||
import eu.siacs.conversations.ui.util.MainThreadExecutor;
|
import eu.siacs.conversations.ui.util.MainThreadExecutor;
|
||||||
|
@ -133,7 +135,7 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private static Set<Media> actionToMedia(final String action) {
|
public static Set<Media> actionToMedia(final String action) {
|
||||||
if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
|
if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
|
||||||
return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
|
return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
|
||||||
} else {
|
} else {
|
||||||
|
@ -416,11 +418,11 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
if (Media.audioOnly(media)) {
|
if (Media.audioOnly(media)) {
|
||||||
final JingleRtpConnection rtpConnection =
|
final JingleRtpConnection rtpConnection =
|
||||||
rtpConnectionReference != null ? rtpConnectionReference.get() : null;
|
rtpConnectionReference != null ? rtpConnectionReference.get() : null;
|
||||||
final AppRTCAudioManager audioManager =
|
final CallIntegration callIntegration =
|
||||||
rtpConnection == null ? null : rtpConnection.getAudioManager();
|
rtpConnection == null ? null : rtpConnection.getCallIntegration();
|
||||||
if (audioManager == null
|
if (callIntegration == null
|
||||||
|| audioManager.getSelectedAudioDevice()
|
|| callIntegration.getSelectedAudioDevice()
|
||||||
== AppRTCAudioManager.AudioDevice.EARPIECE) {
|
== CallIntegration.AudioDevice.EARPIECE) {
|
||||||
acquireProximityWakeLock();
|
acquireProximityWakeLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,8 +468,8 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
private void putProximityWakeLockInProperState(
|
private void putProximityWakeLockInProperState(
|
||||||
final AppRTCAudioManager.AudioDevice audioDevice) {
|
final CallIntegration.AudioDevice audioDevice) {
|
||||||
if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) {
|
if (audioDevice == CallIntegration.AudioDevice.EARPIECE) {
|
||||||
acquireProximityWakeLock();
|
acquireProximityWakeLock();
|
||||||
} else {
|
} else {
|
||||||
releaseProximityWakeLock();
|
releaseProximityWakeLock();
|
||||||
|
@ -581,12 +583,7 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
.getJingleConnectionManager()
|
.getJingleConnectionManager()
|
||||||
.proposeJingleRtpSession(account, with, media);
|
.proposeJingleRtpSession(account, with, media);
|
||||||
} else {
|
} else {
|
||||||
final String sessionId =
|
throw new IllegalStateException("We should not be initializing direct calls from the RtpSessionActivity. Go through CallIntegrationConnectionService.placeCall instead!");
|
||||||
xmppConnectionService
|
|
||||||
.getJingleConnectionManager()
|
|
||||||
.initializeRtpSession(account, with, media);
|
|
||||||
initializeActivityWithRunningRtpSession(account, with, sessionId);
|
|
||||||
resetIntent(account, with, sessionId);
|
|
||||||
}
|
}
|
||||||
putScreenInCallMode(media);
|
putScreenInCallMode(media);
|
||||||
}
|
}
|
||||||
|
@ -1032,10 +1029,10 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
updateInCallButtonConfigurationVideo(
|
updateInCallButtonConfigurationVideo(
|
||||||
rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
|
rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
|
||||||
} else {
|
} else {
|
||||||
final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
|
final CallIntegration callIntegration = requireRtpConnection().getCallIntegration();
|
||||||
updateInCallButtonConfigurationSpeaker(
|
updateInCallButtonConfigurationSpeaker(
|
||||||
audioManager.getSelectedAudioDevice(),
|
callIntegration.getSelectedAudioDevice(),
|
||||||
audioManager.getAudioDevices().size());
|
callIntegration.getAudioDevices().size());
|
||||||
this.binding.inCallActionFarRight.setVisibility(View.GONE);
|
this.binding.inCallActionFarRight.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
if (media.contains(Media.AUDIO)) {
|
if (media.contains(Media.AUDIO)) {
|
||||||
|
@ -1053,7 +1050,7 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private void updateInCallButtonConfigurationSpeaker(
|
private void updateInCallButtonConfigurationSpeaker(
|
||||||
final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
|
final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) {
|
||||||
switch (selectedAudioDevice) {
|
switch (selectedAudioDevice) {
|
||||||
case EARPIECE -> {
|
case EARPIECE -> {
|
||||||
this.binding.inCallActionRight.setImageResource(
|
this.binding.inCallActionRight.setImageResource(
|
||||||
|
@ -1294,19 +1291,19 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
|
|
||||||
private void switchToEarpiece(View view) {
|
private void switchToEarpiece(View view) {
|
||||||
requireRtpConnection()
|
requireRtpConnection()
|
||||||
.getAudioManager()
|
.getCallIntegration()
|
||||||
.setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
|
.setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
|
||||||
acquireProximityWakeLock();
|
acquireProximityWakeLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void switchToSpeaker(View view) {
|
private void switchToSpeaker(View view) {
|
||||||
requireRtpConnection()
|
requireRtpConnection()
|
||||||
.getAudioManager()
|
.getCallIntegration()
|
||||||
.setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
|
.setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
|
||||||
releaseProximityWakeLock();
|
releaseProximityWakeLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void retry(View view) {
|
private void retry(final View view) {
|
||||||
final Intent intent = getIntent();
|
final Intent intent = getIntent();
|
||||||
final Account account = extractAccount(intent);
|
final Account account = extractAccount(intent);
|
||||||
final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
|
final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
|
||||||
|
@ -1315,7 +1312,7 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
|
final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
|
||||||
this.rtpConnectionReference = null;
|
this.rtpConnectionReference = null;
|
||||||
Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
|
Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
|
||||||
proposeJingleRtpSession(account, with, media);
|
CallIntegrationConnectionService.placeCall(this,account,with,media);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exit(final View view) {
|
private void exit(final View view) {
|
||||||
|
@ -1411,8 +1408,8 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioDeviceChanged(
|
public void onAudioDeviceChanged(
|
||||||
final AppRTCAudioManager.AudioDevice selectedAudioDevice,
|
final CallIntegration.AudioDevice selectedAudioDevice,
|
||||||
final Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
|
final Set<CallIntegration.AudioDevice> availableAudioDevices) {
|
||||||
Log.d(
|
Log.d(
|
||||||
Config.LOGTAG,
|
Config.LOGTAG,
|
||||||
"onAudioDeviceChanged in activity: selected:"
|
"onAudioDeviceChanged in activity: selected:"
|
||||||
|
@ -1428,11 +1425,11 @@ public class RtpSessionActivity extends XmppActivity
|
||||||
"onAudioDeviceChanged() nothing to do because end card has been reached");
|
"onAudioDeviceChanged() nothing to do because end card has been reached");
|
||||||
} else {
|
} else {
|
||||||
if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) {
|
if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) {
|
||||||
final AppRTCAudioManager audioManager =
|
final CallIntegration callIntegration =
|
||||||
requireRtpConnection().getAudioManager();
|
requireRtpConnection().getCallIntegration();
|
||||||
updateInCallButtonConfigurationSpeaker(
|
updateInCallButtonConfigurationSpeaker(
|
||||||
audioManager.getSelectedAudioDevice(),
|
callIntegration.getSelectedAudioDevice(),
|
||||||
audioManager.getAudioDevices().size());
|
callIntegration.getAudioDevices().size());
|
||||||
}
|
}
|
||||||
Log.d(
|
Log.d(
|
||||||
Config.LOGTAG,
|
Config.LOGTAG,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.telecom.TelecomManager;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
@ -21,6 +23,8 @@ import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||||
import eu.siacs.conversations.entities.Transferable;
|
import eu.siacs.conversations.entities.Transferable;
|
||||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||||
|
import eu.siacs.conversations.services.CallIntegration;
|
||||||
|
import eu.siacs.conversations.services.CallIntegrationConnectionService;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
@ -135,6 +139,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
connections.put(id, connection);
|
connections.put(id, connection);
|
||||||
|
|
||||||
|
CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
|
||||||
|
|
||||||
mXmppConnectionService.updateConversationUi();
|
mXmppConnectionService.updateConversationUi();
|
||||||
connection.deliverPacket(packet);
|
connection.deliverPacket(packet);
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,12 +155,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBusy() {
|
public boolean isBusy() {
|
||||||
if (mXmppConnectionService.isPhoneInCall()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (AbstractJingleConnection connection : this.connections.values()) {
|
for (AbstractJingleConnection connection : this.connections.values()) {
|
||||||
if (connection instanceof JingleRtpConnection) {
|
if (connection instanceof JingleRtpConnection) {
|
||||||
if (((JingleRtpConnection) connection).isTerminated()) {
|
if (connection.isTerminated()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -181,17 +185,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyPhoneCallStarted() {
|
|
||||||
for (AbstractJingleConnection connection : connections.values()) {
|
|
||||||
if (connection instanceof JingleRtpConnection rtpConnection) {
|
|
||||||
if (rtpConnection.isTerminated()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
rtpConnection.notifyPhoneCall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<RtpSessionProposal> findMatchingSessionProposal(
|
private Optional<RtpSessionProposal> findMatchingSessionProposal(
|
||||||
final Account account, final Jid with, final Set<Media> media) {
|
final Account account, final Jid with, final Set<Media> media) {
|
||||||
synchronized (this.rtpSessionProposals) {
|
synchronized (this.rtpSessionProposals) {
|
||||||
|
@ -390,6 +383,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
this.connections.put(id, rtpConnection);
|
this.connections.put(id, rtpConnection);
|
||||||
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
|
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
|
||||||
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
|
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
|
||||||
|
|
||||||
|
CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
|
||||||
// TODO actually do the automatic accept?!
|
// TODO actually do the automatic accept?!
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
Log.d(
|
||||||
|
@ -439,6 +434,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
this.connections.put(id, rtpConnection);
|
this.connections.put(id, rtpConnection);
|
||||||
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
|
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
|
||||||
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
|
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
|
||||||
|
|
||||||
|
CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
Log.d(
|
||||||
|
@ -457,7 +454,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
if (proposal != null) {
|
if (proposal != null) {
|
||||||
rtpSessionProposals.remove(proposal);
|
rtpSessionProposals.remove(proposal);
|
||||||
final JingleRtpConnection rtpConnection =
|
final JingleRtpConnection rtpConnection =
|
||||||
new JingleRtpConnection(this, id, account.getJid());
|
new JingleRtpConnection(this, id, account.getJid(), proposal.callIntegration);
|
||||||
rtpConnection.setProposedMedia(proposal.media);
|
rtpConnection.setProposedMedia(proposal.media);
|
||||||
this.connections.put(id, rtpConnection);
|
this.connections.put(id, rtpConnection);
|
||||||
rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
|
rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED);
|
||||||
|
@ -490,6 +487,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
getRtpSessionProposal(account, from.asBareJid(), sessionId);
|
getRtpSessionProposal(account, from.asBareJid(), sessionId);
|
||||||
synchronized (rtpSessionProposals) {
|
synchronized (rtpSessionProposals) {
|
||||||
if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
|
if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
|
||||||
|
proposal.callIntegration.busy();
|
||||||
writeLogMissedOutgoing(
|
writeLogMissedOutgoing(
|
||||||
account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
|
account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
|
||||||
toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
|
toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
|
||||||
|
@ -628,10 +626,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
}
|
}
|
||||||
|
|
||||||
void finishConnection(final AbstractJingleConnection connection) {
|
|
||||||
this.connections.remove(connection.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
void finishConnectionOrThrow(final AbstractJingleConnection connection) {
|
void finishConnectionOrThrow(final AbstractJingleConnection connection) {
|
||||||
final AbstractJingleConnection.Id id = connection.getId();
|
final AbstractJingleConnection.Id id = connection.getId();
|
||||||
if (this.connections.remove(id) == null) {
|
if (this.connections.remove(id) == null) {
|
||||||
|
@ -680,6 +674,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
+ ": retracting rtp session proposal with "
|
+ ": retracting rtp session proposal with "
|
||||||
+ rtpSessionProposal.with);
|
+ rtpSessionProposal.with);
|
||||||
this.rtpSessionProposals.remove(rtpSessionProposal);
|
this.rtpSessionProposals.remove(rtpSessionProposal);
|
||||||
|
rtpSessionProposal.callIntegration.retracted();
|
||||||
final MessagePacket messagePacket =
|
final MessagePacket messagePacket =
|
||||||
mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
|
mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
|
||||||
writeLogMissedOutgoing(
|
writeLogMissedOutgoing(
|
||||||
|
@ -691,7 +686,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
mXmppConnectionService.sendMessagePacket(account, messagePacket);
|
mXmppConnectionService.sendMessagePacket(account, messagePacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String initializeRtpSession(
|
public JingleRtpConnection initializeRtpSession(
|
||||||
final Account account, final Jid with, final Set<Media> media) {
|
final Account account, final Jid with, final Set<Media> media) {
|
||||||
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
|
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
|
||||||
final JingleRtpConnection rtpConnection =
|
final JingleRtpConnection rtpConnection =
|
||||||
|
@ -699,15 +694,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
rtpConnection.setProposedMedia(media);
|
rtpConnection.setProposedMedia(media);
|
||||||
this.connections.put(id, rtpConnection);
|
this.connections.put(id, rtpConnection);
|
||||||
rtpConnection.sendSessionInitiate();
|
rtpConnection.sendSessionInitiate();
|
||||||
return id.sessionId;
|
return rtpConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void proposeJingleRtpSession(
|
public RtpSessionProposal proposeJingleRtpSession(
|
||||||
final Account account, final Jid with, final Set<Media> media) {
|
final Account account, final Jid with, final Set<Media> media) {
|
||||||
synchronized (this.rtpSessionProposals) {
|
synchronized (this.rtpSessionProposals) {
|
||||||
for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
|
for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
|
||||||
this.rtpSessionProposals.entrySet()) {
|
this.rtpSessionProposals.entrySet()) {
|
||||||
RtpSessionProposal proposal = entry.getKey();
|
final RtpSessionProposal proposal = entry.getKey();
|
||||||
if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
|
if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
|
||||||
final DeviceDiscoveryState preexistingState = entry.getValue();
|
final DeviceDiscoveryState preexistingState = entry.getValue();
|
||||||
if (preexistingState != null
|
if (preexistingState != null
|
||||||
|
@ -716,7 +711,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
toneManager.transition(endUserState, media);
|
toneManager.transition(endUserState, media);
|
||||||
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
|
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
|
||||||
account, with, proposal.sessionId, endUserState);
|
account, with, proposal.sessionId, endUserState);
|
||||||
return;
|
return proposal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -725,19 +720,23 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
Log.d(
|
Log.d(
|
||||||
Config.LOGTAG,
|
Config.LOGTAG,
|
||||||
"ignoring request to propose jingle session because the other party already created one for us");
|
"ignoring request to propose jingle session because the other party already created one for us");
|
||||||
return;
|
// TODO return something that we can parse the connection of of
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"There is already a running RTP session. This should have been caught by the UI");
|
"There is already a running RTP session. This should have been caught by the UI");
|
||||||
}
|
}
|
||||||
|
final CallIntegration callIntegration = new CallIntegration(mXmppConnectionService.getApplicationContext());
|
||||||
|
callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
|
||||||
final RtpSessionProposal proposal =
|
final RtpSessionProposal proposal =
|
||||||
RtpSessionProposal.of(account, with.asBareJid(), media);
|
RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration);
|
||||||
this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
|
this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
|
||||||
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
|
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
|
||||||
account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE);
|
account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE);
|
||||||
final MessagePacket messagePacket =
|
final MessagePacket messagePacket =
|
||||||
mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
|
mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
|
||||||
mXmppConnectionService.sendMessagePacket(account, messagePacket);
|
mXmppConnectionService.sendMessagePacket(account, messagePacket);
|
||||||
|
return proposal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -826,6 +825,21 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JingleRtpConnection findJingleRtpConnection(final Account account, final Jid with) {
|
||||||
|
for (final AbstractJingleConnection connection : this.connections.values()) {
|
||||||
|
if (connection instanceof JingleRtpConnection rtpConnection) {
|
||||||
|
if (rtpConnection.isTerminated()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final var id = rtpConnection.getId();
|
||||||
|
if (id.account == account && account.getJid().equals(with)) {
|
||||||
|
return rtpConnection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void resendSessionProposals(final Account account) {
|
private void resendSessionProposals(final Account account) {
|
||||||
synchronized (this.rtpSessionProposals) {
|
synchronized (this.rtpSessionProposals) {
|
||||||
for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
|
for (final Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
|
||||||
|
@ -865,7 +879,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
this.rtpSessionProposals.put(sessionProposal, target);
|
this.rtpSessionProposals.put(sessionProposal, target);
|
||||||
final RtpEndUserState endUserState = target.toEndUserState();
|
final RtpEndUserState endUserState = target.toEndUserState();
|
||||||
toneManager.transition(endUserState, sessionProposal.media);
|
if (endUserState == RtpEndUserState.RINGING) {
|
||||||
|
sessionProposal.callIntegration.setDialing();
|
||||||
|
}
|
||||||
|
//toneManager.transition(endUserState, sessionProposal.media);
|
||||||
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
|
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
|
||||||
account, sessionProposal.with, sessionProposal.sessionId, endUserState);
|
account, sessionProposal.with, sessionProposal.sessionId, endUserState);
|
||||||
Log.d(
|
Log.d(
|
||||||
|
@ -994,16 +1011,18 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
public final String sessionId;
|
public final String sessionId;
|
||||||
public final Set<Media> media;
|
public final Set<Media> media;
|
||||||
private final Account account;
|
private final Account account;
|
||||||
|
private final CallIntegration callIntegration;
|
||||||
|
|
||||||
private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media) {
|
private RtpSessionProposal(Account account, Jid with, String sessionId, Set<Media> media, final CallIntegration callIntegration) {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.with = with;
|
this.with = with;
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.media = media;
|
this.media = media;
|
||||||
|
this.callIntegration = callIntegration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RtpSessionProposal of(Account account, Jid with, Set<Media> media) {
|
public static RtpSessionProposal of(Account account, Jid with, Set<Media> media, final CallIntegration callIntegration) {
|
||||||
return new RtpSessionProposal(account, with, nextRandomId(), media);
|
return new RtpSessionProposal(account, with, nextRandomId(), media,callIntegration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1035,5 +1054,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallIntegration getCallIntegration() {
|
||||||
|
return this.callIntegration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import android.telecom.Call;
|
||||||
|
import android.telecom.TelecomManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -12,13 +14,11 @@ import com.google.common.base.Stopwatch;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.google.common.collect.ImmutableMultimap;
|
import com.google.common.collect.ImmutableMultimap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.common.primitives.Ints;
|
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
@ -34,7 +34,7 @@ import eu.siacs.conversations.entities.Conversational;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||||
import eu.siacs.conversations.utils.IP;
|
import eu.siacs.conversations.services.CallIntegration;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
@ -67,7 +67,7 @@ import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class JingleRtpConnection extends AbstractJingleConnection
|
public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
implements WebRTCWrapper.EventCallback {
|
implements WebRTCWrapper.EventCallback, CallIntegration.Callback {
|
||||||
|
|
||||||
public static final List<State> STATES_SHOWING_ONGOING_CALL =
|
public static final List<State> STATES_SHOWING_ONGOING_CALL =
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
|
@ -78,6 +78,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
private final Queue<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>>
|
private final Queue<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>>
|
||||||
pendingIceCandidates = new LinkedList<>();
|
pendingIceCandidates = new LinkedList<>();
|
||||||
private final OmemoVerification omemoVerification = new OmemoVerification();
|
private final OmemoVerification omemoVerification = new OmemoVerification();
|
||||||
|
private final CallIntegration callIntegration;
|
||||||
private final Message message;
|
private final Message message;
|
||||||
|
|
||||||
private Set<Media> proposedMedia;
|
private Set<Media> proposedMedia;
|
||||||
|
@ -90,7 +91,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
|
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
|
||||||
private ScheduledFuture<?> ringingTimeoutFuture;
|
private ScheduledFuture<?> ringingTimeoutFuture;
|
||||||
|
|
||||||
JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
|
JingleRtpConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) {
|
||||||
|
this(jingleConnectionManager, id, initiator, new CallIntegration(jingleConnectionManager.getXmppConnectionService().getApplicationContext()));
|
||||||
|
this.callIntegration.setAddress(CallIntegration.address(id.with.asBareJid()), TelecomManager.PRESENTATION_ALLOWED);
|
||||||
|
this.callIntegration.setInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
JingleRtpConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator, final CallIntegration callIntegration) {
|
||||||
super(jingleConnectionManager, id, initiator);
|
super(jingleConnectionManager, id, initiator);
|
||||||
final Conversation conversation =
|
final Conversation conversation =
|
||||||
jingleConnectionManager
|
jingleConnectionManager
|
||||||
|
@ -102,6 +109,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED,
|
isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED,
|
||||||
Message.TYPE_RTP_SESSION,
|
Message.TYPE_RTP_SESSION,
|
||||||
id.sessionId);
|
id.sessionId);
|
||||||
|
this.callIntegration = callIntegration;
|
||||||
|
this.callIntegration.setCallback(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1158,6 +1167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
target = State.SESSION_INITIALIZED_PRE_APPROVED;
|
target = State.SESSION_INITIALIZED_PRE_APPROVED;
|
||||||
} else {
|
} else {
|
||||||
target = State.SESSION_INITIALIZED;
|
target = State.SESSION_INITIALIZED;
|
||||||
|
setProposedMedia(contentMap.getMedia());
|
||||||
}
|
}
|
||||||
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
|
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
|
||||||
respondOk(jinglePacket);
|
respondOk(jinglePacket);
|
||||||
|
@ -1628,7 +1638,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
+ from
|
+ from
|
||||||
+ " for "
|
+ " for "
|
||||||
+ media);
|
+ media);
|
||||||
this.proposedMedia = Sets.newHashSet(media);
|
this.setProposedMedia(Sets.newHashSet(media));
|
||||||
})) {
|
})) {
|
||||||
if (serverMsgId != null) {
|
if (serverMsgId != null) {
|
||||||
this.message.setServerMsgId(serverMsgId);
|
this.message.setServerMsgId(serverMsgId);
|
||||||
|
@ -1648,6 +1658,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startRinging() {
|
private void startRinging() {
|
||||||
|
this.callIntegration.setRinging();
|
||||||
Log.d(
|
Log.d(
|
||||||
Config.LOGTAG,
|
Config.LOGTAG,
|
||||||
id.account.getJid().asBareJid()
|
id.account.getJid().asBareJid()
|
||||||
|
@ -1657,6 +1668,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
ringingTimeoutFuture =
|
ringingTimeoutFuture =
|
||||||
jingleConnectionManager.schedule(
|
jingleConnectionManager.schedule(
|
||||||
this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS);
|
this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS);
|
||||||
|
if (CallIntegration.selfManaged()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
xmppConnectionService.getNotificationService().startRinging(id, getMedia());
|
xmppConnectionService.getNotificationService().startRinging(id, getMedia());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2054,6 +2068,56 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isPeerConnectionConnected() {
|
||||||
|
try {
|
||||||
|
return webRTCWrapper.getState() == PeerConnection.PeerConnectionState.CONNECTED;
|
||||||
|
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCallIntegrationState() {
|
||||||
|
switch (this.state) {
|
||||||
|
case NULL, PROPOSED, SESSION_INITIALIZED -> {
|
||||||
|
if (isInitiator()) {
|
||||||
|
this.callIntegration.setDialing();
|
||||||
|
} else {
|
||||||
|
this.callIntegration.setRinging();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PROCEED, SESSION_INITIALIZED_PRE_APPROVED -> {
|
||||||
|
if (isInitiator()) {
|
||||||
|
this.callIntegration.setDialing();
|
||||||
|
} else {
|
||||||
|
this.callIntegration.setInitialized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SESSION_ACCEPTED -> {
|
||||||
|
if (isPeerConnectionConnected()) {
|
||||||
|
this.callIntegration.setActive();
|
||||||
|
} else {
|
||||||
|
this.callIntegration.setInitialized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case REJECTED, REJECTED_RACED, TERMINATED_DECLINED_OR_BUSY -> {
|
||||||
|
if (isInitiator()) {
|
||||||
|
this.callIntegration.busy();
|
||||||
|
} else {
|
||||||
|
this.callIntegration.rejected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case TERMINATED_SUCCESS -> this.callIntegration.success();
|
||||||
|
case ACCEPTED -> this.callIntegration.accepted();
|
||||||
|
case RETRACTED, RETRACTED_RACED, TERMINATED_CANCEL_OR_TIMEOUT -> this.callIntegration
|
||||||
|
.retracted();
|
||||||
|
case TERMINATED_CONNECTIVITY_ERROR,
|
||||||
|
TERMINATED_APPLICATION_FAILURE,
|
||||||
|
TERMINATED_SECURITY_ERROR -> this.callIntegration.error();
|
||||||
|
default -> throw new IllegalStateException(
|
||||||
|
String.format("%s is not handled", this.state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ContentAddition getPendingContentAddition() {
|
public ContentAddition getPendingContentAddition() {
|
||||||
final RtpContentMap in = this.incomingContentAdd;
|
final RtpContentMap in = this.incomingContentAdd;
|
||||||
final RtpContentMap out = this.outgoingContentAdd;
|
final RtpContentMap out = this.outgoingContentAdd;
|
||||||
|
@ -2135,15 +2199,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyPhoneCall() {
|
|
||||||
Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
|
|
||||||
if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
|
|
||||||
rejectCall();
|
|
||||||
} else {
|
|
||||||
endCall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void rejectCall() {
|
public synchronized void rejectCall() {
|
||||||
if (isTerminated()) {
|
if (isTerminated()) {
|
||||||
Log.w(
|
Log.w(
|
||||||
|
@ -2537,8 +2592,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
private void modifyLocalContentMap(final RtpContentMap rtpContentMap) {
|
private void modifyLocalContentMap(final RtpContentMap rtpContentMap) {
|
||||||
final RtpContentMap activeContents = rtpContentMap.activeContents();
|
final RtpContentMap activeContents = rtpContentMap.activeContents();
|
||||||
setLocalContentMap(activeContents);
|
setLocalContentMap(activeContents);
|
||||||
this.webRTCWrapper.switchSpeakerPhonePreference(
|
// TODO change audio device on callIntegration was (`switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia())`)
|
||||||
AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia()));
|
|
||||||
updateEndUserState();
|
updateEndUserState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2571,8 +2625,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
|
return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AppRTCAudioManager getAudioManager() {
|
|
||||||
return webRTCWrapper.getAudioManager();
|
public CallIntegration getCallIntegration() {
|
||||||
|
return this.callIntegration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMicrophoneEnabled() {
|
public boolean isMicrophoneEnabled() {
|
||||||
|
@ -2603,10 +2658,26 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
return webRTCWrapper.switchCamera();
|
return webRTCWrapper.switchCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCallIntegrationShowIncomingCallUi() {
|
||||||
|
xmppConnectionService.getNotificationService().startRinging(id, getMedia());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCallIntegrationDisconnect() {
|
||||||
|
Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections");
|
||||||
|
if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) {
|
||||||
|
rejectCall();
|
||||||
|
} else {
|
||||||
|
endCall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioDeviceChanged(
|
public void onAudioDeviceChanged(
|
||||||
AppRTCAudioManager.AudioDevice selectedAudioDevice,
|
final CallIntegration.AudioDevice selectedAudioDevice,
|
||||||
Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
|
final Set<CallIntegration.AudioDevice> availableAudioDevices) {
|
||||||
|
Log.d(Config.LOGTAG,"onAudioDeviceChanged("+selectedAudioDevice+","+availableAudioDevices+")");
|
||||||
xmppConnectionService.notifyJingleRtpConnectionUpdate(
|
xmppConnectionService.notifyJingleRtpConnectionUpdate(
|
||||||
selectedAudioDevice, availableAudioDevices);
|
selectedAudioDevice, availableAudioDevices);
|
||||||
}
|
}
|
||||||
|
@ -2614,6 +2685,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
private void updateEndUserState() {
|
private void updateEndUserState() {
|
||||||
final RtpEndUserState endUserState = getEndUserState();
|
final RtpEndUserState endUserState = getEndUserState();
|
||||||
jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
|
jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
|
||||||
|
this.updateCallIntegrationState();
|
||||||
xmppConnectionService.notifyJingleRtpConnectionUpdate(
|
xmppConnectionService.notifyJingleRtpConnectionUpdate(
|
||||||
id.account, id.with, id.sessionId, endUserState);
|
id.account, id.with, id.sessionId, endUserState);
|
||||||
}
|
}
|
||||||
|
@ -2670,6 +2742,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
protected void finish() {
|
protected void finish() {
|
||||||
if (isTerminated()) {
|
if (isTerminated()) {
|
||||||
this.cancelRingingTimeout();
|
this.cancelRingingTimeout();
|
||||||
|
this.callIntegration.verifyDisconnected();
|
||||||
this.webRTCWrapper.verifyClosed();
|
this.webRTCWrapper.verifyClosed();
|
||||||
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
|
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
|
||||||
super.finish();
|
super.finish();
|
||||||
|
@ -2724,6 +2797,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
|
|
||||||
void setProposedMedia(final Set<Media> media) {
|
void setProposedMedia(final Set<Media> media) {
|
||||||
this.proposedMedia = media;
|
this.proposedMedia = media;
|
||||||
|
this.callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fireStateUpdate() {
|
public void fireStateUpdate() {
|
||||||
|
|
|
@ -9,7 +9,7 @@ public enum RtpEndUserState {
|
||||||
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
|
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
|
||||||
RINGING, //'propose' has been sent out and it has been 184 acked
|
RINGING, //'propose' has been sent out and it has been 184 acked
|
||||||
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
|
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
|
||||||
ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through
|
ENDING_CALL, //libwebrt says 'closed' but session-terminate has not gone through
|
||||||
ENDED, //close UI
|
ENDED, //close UI
|
||||||
DECLINED_OR_BUSY, //other party declined; no retry button
|
DECLINED_OR_BUSY, //other party declined; no retry button
|
||||||
CONNECTIVITY_ERROR, //network error; retry button
|
CONNECTIVITY_ERROR, //network error; retry button
|
||||||
|
|
|
@ -89,7 +89,8 @@ class ToneManager {
|
||||||
}
|
}
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case RINGING:
|
case RINGING:
|
||||||
scheduleWaitingTone();
|
// ringing can be removed as this is now handled by 'CallIntegration'
|
||||||
|
//scheduleWaitingTone();
|
||||||
break;
|
break;
|
||||||
case CONNECTED:
|
case CONNECTED:
|
||||||
scheduleConnected();
|
scheduleConnected();
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||||
|
import eu.siacs.conversations.services.CallIntegration;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
|
|
||||||
import org.webrtc.AudioSource;
|
import org.webrtc.AudioSource;
|
||||||
|
@ -83,16 +84,6 @@ public class WebRTCWrapper {
|
||||||
private final EventCallback eventCallback;
|
private final EventCallback eventCallback;
|
||||||
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
|
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
|
||||||
private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
|
private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
|
||||||
private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents =
|
|
||||||
new AppRTCAudioManager.AudioManagerEvents() {
|
|
||||||
@Override
|
|
||||||
public void onAudioDeviceChanged(
|
|
||||||
AppRTCAudioManager.AudioDevice selectedAudioDevice,
|
|
||||||
Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
|
|
||||||
eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
||||||
private TrackWrapper<AudioTrack> localAudioTrack = null;
|
private TrackWrapper<AudioTrack> localAudioTrack = null;
|
||||||
private TrackWrapper<VideoTrack> localVideoTrack = null;
|
private TrackWrapper<VideoTrack> localVideoTrack = null;
|
||||||
private VideoTrack remoteVideoTrack = null;
|
private VideoTrack remoteVideoTrack = null;
|
||||||
|
@ -214,7 +205,6 @@ public class WebRTCWrapper {
|
||||||
};
|
};
|
||||||
@Nullable private PeerConnectionFactory peerConnectionFactory = null;
|
@Nullable private PeerConnectionFactory peerConnectionFactory = null;
|
||||||
@Nullable private PeerConnection peerConnection = null;
|
@Nullable private PeerConnection peerConnection = null;
|
||||||
private AppRTCAudioManager appRTCAudioManager = null;
|
|
||||||
private ToneManager toneManager = null;
|
private ToneManager toneManager = null;
|
||||||
private Context context = null;
|
private Context context = null;
|
||||||
private EglBase eglBase = null;
|
private EglBase eglBase = null;
|
||||||
|
@ -251,15 +241,6 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
this.context = service;
|
this.context = service;
|
||||||
this.toneManager = service.getJingleConnectionManager().toneManager;
|
this.toneManager = service.getJingleConnectionManager().toneManager;
|
||||||
mainHandler.post(
|
|
||||||
() -> {
|
|
||||||
appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
|
|
||||||
toneManager.setAppRtcAudioManagerHasControl(true);
|
|
||||||
appRTCAudioManager.start(audioManagerEvents);
|
|
||||||
eventCallback.onAudioDeviceChanged(
|
|
||||||
appRTCAudioManager.getSelectedAudioDevice(),
|
|
||||||
appRTCAudioManager.getAudioDevices());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void initializePeerConnection(
|
synchronized void initializePeerConnection(
|
||||||
|
@ -462,16 +443,11 @@ public class WebRTCWrapper {
|
||||||
final PeerConnection peerConnection = this.peerConnection;
|
final PeerConnection peerConnection = this.peerConnection;
|
||||||
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
|
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
|
||||||
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
|
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
|
||||||
final AppRTCAudioManager audioManager = this.appRTCAudioManager;
|
|
||||||
final EglBase eglBase = this.eglBase;
|
final EglBase eglBase = this.eglBase;
|
||||||
if (peerConnection != null) {
|
if (peerConnection != null) {
|
||||||
this.peerConnection = null;
|
this.peerConnection = null;
|
||||||
dispose(peerConnection);
|
dispose(peerConnection);
|
||||||
}
|
}
|
||||||
if (audioManager != null) {
|
|
||||||
toneManager.setAppRtcAudioManagerHasControl(false);
|
|
||||||
mainHandler.post(audioManager::stop);
|
|
||||||
}
|
|
||||||
this.localVideoTrack = null;
|
this.localVideoTrack = null;
|
||||||
this.remoteVideoTrack = null;
|
this.remoteVideoTrack = null;
|
||||||
if (videoSourceWrapper != null) {
|
if (videoSourceWrapper != null) {
|
||||||
|
@ -498,8 +474,8 @@ public class WebRTCWrapper {
|
||||||
|| this.eglBase != null
|
|| this.eglBase != null
|
||||||
|| this.localVideoTrack != null
|
|| this.localVideoTrack != null
|
||||||
|| this.remoteVideoTrack != null) {
|
|| this.remoteVideoTrack != null) {
|
||||||
final IllegalStateException e =
|
final AssertionError e =
|
||||||
new IllegalStateException("WebRTCWrapper hasn't been closed properly");
|
new AssertionError("WebRTCWrapper hasn't been closed properly");
|
||||||
Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
|
Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -750,27 +726,15 @@ public class WebRTCWrapper {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppRTCAudioManager getAudioManager() {
|
|
||||||
return appRTCAudioManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
void execute(final Runnable command) {
|
void execute(final Runnable command) {
|
||||||
this.executorService.execute(command);
|
this.executorService.execute(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) {
|
|
||||||
mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference));
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface EventCallback {
|
public interface EventCallback {
|
||||||
void onIceCandidate(IceCandidate iceCandidate);
|
void onIceCandidate(IceCandidate iceCandidate);
|
||||||
|
|
||||||
void onConnectionChange(PeerConnection.PeerConnectionState newState);
|
void onConnectionChange(PeerConnection.PeerConnectionState newState);
|
||||||
|
|
||||||
void onAudioDeviceChanged(
|
|
||||||
AppRTCAudioManager.AudioDevice selectedAudioDevice,
|
|
||||||
Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
|
|
||||||
|
|
||||||
void onRenegotiationNeeded();
|
void onRenegotiationNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue