select audio device (WIP)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2022-02-01 00:42:44 +01:00
parent 9b889d232f
commit 9c0fa9acc2
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
3 changed files with 77 additions and 143 deletions

View File

@ -32,7 +32,6 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
import android.media.AudioAttributes; import android.media.AudioAttributes;
import android.media.MediaPlayer; import android.media.MediaPlayer;
@ -84,7 +83,6 @@ import com.nextcloud.talk.models.json.signaling.SignalingOverall;
import com.nextcloud.talk.models.json.signaling.settings.IceServer; import com.nextcloud.talk.models.json.signaling.settings.IceServer;
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall; import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
import com.nextcloud.talk.ui.dialog.AudioOutputDialog; import com.nextcloud.talk.ui.dialog.AudioOutputDialog;
import com.nextcloud.talk.ui.dialog.ScopeDialog;
import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.DisplayUtils; import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.NotificationUtils; import com.nextcloud.talk.utils.NotificationUtils;
@ -145,6 +143,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.graphics.drawable.DrawableCompat;
import autodagger.AutoInjector; import autodagger.AutoInjector;
import io.reactivex.Observable; import io.reactivex.Observable;
@ -335,17 +334,6 @@ public class CallActivity extends CallBaseActivity {
this this
).show()); ).show());
// binding.audioOutputButton.setOnClickListener(l -> {
// if (audioManager != null) {
// audioManager.toggleUseSpeakerphone();
// if (audioManager.isSpeakerphoneAutoOn()) {
// binding.audioOutputButton.getHierarchy().setPlaceholderImage(R.drawable.ic_volume_up_white_24dp);
// } else {
// binding.audioOutputButton.getHierarchy().setPlaceholderImage(R.drawable.ic_volume_mute_white_24dp);
// }
// }
// });
binding.microphoneButton.setOnClickListener(l -> onMicrophoneClick()); binding.microphoneButton.setOnClickListener(l -> onMicrophoneClick());
binding.microphoneButton.setOnLongClickListener(l -> { binding.microphoneButton.setOnLongClickListener(l -> {
if (!microphoneOn) { if (!microphoneOn) {
@ -381,9 +369,31 @@ public class CallActivity extends CallBaseActivity {
}); });
} }
public void setAudioOutputIcon(Drawable drawable){ public void setAudioOutputChannel(MagicAudioManager.AudioDevice audioDevice) {
binding.audioOutputButton.getHierarchy().setPlaceholderImage(drawable); if (audioManager == null) {
DrawableCompat.setTint(drawable, Color.WHITE); return;
}
audioManager.selectAudioDevice(audioDevice);
switch (audioManager.getResultingAudioDevice()) {
case BLUETOOTH:
binding.audioOutputButton.getHierarchy().setPlaceholderImage(
AppCompatResources.getDrawable(context, R.drawable.ic_baseline_bluetooth_audio_24));
break;
case SPEAKER_PHONE:
binding.audioOutputButton.getHierarchy().setPlaceholderImage(
AppCompatResources.getDrawable(context, R.drawable.ic_volume_up_white_24dp));
break;
case EARPIECE:
binding.audioOutputButton.getHierarchy().setPlaceholderImage(
AppCompatResources.getDrawable(context, R.drawable.ic_baseline_phone_in_talk_24));
break;
default:
Log.e(TAG, "Invalid audio device selection");
break;
}
DrawableCompat.setTint(binding.audioOutputButton.getDrawable(), Color.WHITE);
} }
private void createCameraEnumerator() { private void createCameraEnumerator() {
@ -391,7 +401,7 @@ public class CallActivity extends CallBaseActivity {
try { try {
camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(this); camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(this);
} catch (final Throwable throwable) { } catch (final Throwable throwable) {
Log.w(TAG, "Camera2Enumator threw an error"); Log.w(TAG, "Camera2Enumerator threw an error");
} }
if (camera2EnumeratorIsSupported) { if (camera2EnumeratorIsSupported) {
@ -726,7 +736,8 @@ public class CallActivity extends CallBaseActivity {
} }
private void onAudioManagerDevicesChanged( private void onAudioManagerDevicesChanged(
final MagicAudioManager.AudioDevice device, final Set<MagicAudioManager.AudioDevice> availableDevices) { final MagicAudioManager.AudioDevice device,
final Set<MagicAudioManager.AudioDevice> availableDevices) {
Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", " Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
+ "selected: " + device); + "selected: " + device);

View File

@ -23,7 +23,6 @@
package com.nextcloud.talk.ui.dialog package com.nextcloud.talk.ui.dialog
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -31,6 +30,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.activities.CallActivity
import com.nextcloud.talk.databinding.DialogAudioOutputBinding import com.nextcloud.talk.databinding.DialogAudioOutputBinding
import com.nextcloud.talk.webrtc.MagicAudioManager
class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(callActivity) { class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(callActivity) {
@ -44,22 +44,17 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
dialogAudioOutputBinding.audioOutputBluetooth.setOnClickListener { dialogAudioOutputBinding.audioOutputBluetooth.setOnClickListener {
Log.d(TAG, "bluetooth button clicked") callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.BLUETOOTH)
callActivity.setAudioOutputIcon(dialogAudioOutputBinding.audioOutputBluetoothIcon.drawable)
dismiss() dismiss()
} }
dialogAudioOutputBinding.audioOutputSpeaker.setOnClickListener { dialogAudioOutputBinding.audioOutputSpeaker.setOnClickListener {
Log.d(TAG, "speaker button clicked") callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.SPEAKER_PHONE)
callActivity.setAudioOutputIcon(dialogAudioOutputBinding.audioOutputSpeakerIcon.drawable)
dismiss() dismiss()
} }
dialogAudioOutputBinding.audioOutputEarspeaker.setOnClickListener { dialogAudioOutputBinding.audioOutputEarspeaker.setOnClickListener {
Log.d(TAG, "earspeaker button clicked") callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.EARPIECE)
callActivity.setAudioOutputIcon(dialogAudioOutputBinding.audioOutputEarspeakerIcon.drawable)
dismiss() dismiss()
} }
} }

View File

@ -55,13 +55,9 @@ import java.util.Set;
*/ */
public class MagicAudioManager { public class MagicAudioManager {
private static final String TAG = "MagicAudioManager"; private static final String TAG = "MagicAudioManager";
private static final String SPEAKERPHONE_AUTO = "auto";
private static final String SPEAKERPHONE_FALSE = "false";
private final Context magicContext; private final Context magicContext;
// Handles all tasks related to Bluetooth headset devices.
private final MagicBluetoothManager bluetoothManager; private final MagicBluetoothManager bluetoothManager;
// Contains speakerphone setting: auto, true or false private boolean controlSpeakerByProximitySensor;
private String useSpeakerphone;
private AudioManager audioManager; private AudioManager audioManager;
private AudioManagerEvents audioManagerEvents; private AudioManagerEvents audioManagerEvents;
private AudioManagerState amState; private AudioManagerState amState;
@ -69,31 +65,15 @@ public class MagicAudioManager {
private boolean savedIsSpeakerPhoneOn = false; private boolean savedIsSpeakerPhoneOn = false;
private boolean savedIsMicrophoneMute = false; private boolean savedIsMicrophoneMute = false;
private boolean hasWiredHeadset = false; private boolean hasWiredHeadset = false;
// Default audio device; speaker phone for video calls or earpiece for audio
// only calls.
private AudioDevice defaultAudioDevice;
// Contains the currently selected audio device.
// 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
// user to explicitly select a device (and overrid any predefined scheme).
// See |userSelectedAudioDevice| for details.
private AudioDevice selectedAudioDevice;
// Contains the user-selected audio device which overrides the predefined
// selection scheme.
// TODO(henrika): always set to AudioDevice.NONE today. Add support for
// explicit selection based on choice by userSelectedAudioDevice.
private AudioDevice userSelectedAudioDevice; private AudioDevice userSelectedAudioDevice;
// Proximity sensor object. It measures the proximity of an object in cm private AudioDevice resultingAudioDevice;
// relative to the view screen of a device and can therefore be used to
// assist device switching (close to ear <=> use headset earpiece if
// available, far from ear <=> use speaker phone).
private MagicProximitySensor proximitySensor = null; private MagicProximitySensor proximitySensor = null;
// Contains a list of available audio devices. A Set collection is used to
// avoid duplicate elements.
private Set<AudioDevice> audioDevices = new HashSet<>(); private Set<AudioDevice> audioDevices = new HashSet<>();
// Broadcast receiver for wired headset intent broadcasts.
private BroadcastReceiver wiredHeadsetReceiver; private BroadcastReceiver wiredHeadsetReceiver;
// Callback method for changes in audio focus.
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
private PowerManagerUtils powerManagerUtils; private PowerManagerUtils powerManagerUtils;
@ -110,18 +90,8 @@ public class MagicAudioManager {
powerManagerUtils = new PowerManagerUtils(); powerManagerUtils = new PowerManagerUtils();
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK); powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK);
if (useProximitySensor) { controlSpeakerByProximitySensor = useProximitySensor;
useSpeakerphone = SPEAKERPHONE_AUTO; updateAudioDeviceState();
} else {
useSpeakerphone = SPEAKERPHONE_FALSE;
}
if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) {
defaultAudioDevice = AudioDevice.EARPIECE;
} else {
defaultAudioDevice = 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.
@ -134,8 +104,6 @@ public class MagicAudioManager {
onProximitySensorChangedState(); onProximitySensorChangedState();
} }
}); });
Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice);
} }
/** /**
@ -145,29 +113,13 @@ public class MagicAudioManager {
return new MagicAudioManager(context, useProximitySensor); return new MagicAudioManager(context, useProximitySensor);
} }
public void toggleUseSpeakerphone() {
if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) {
useSpeakerphone = SPEAKERPHONE_AUTO;
setDefaultAudioDevice(AudioDevice.SPEAKER_PHONE);
} else {
useSpeakerphone = SPEAKERPHONE_FALSE;
setDefaultAudioDevice(AudioDevice.EARPIECE);
}
updateAudioDeviceState();
}
public boolean isSpeakerphoneAutoOn() {
return (useSpeakerphone.equals(SPEAKERPHONE_AUTO));
}
/** /**
* This method is called when the proximity sensor reports a state change, * This method is called when the proximity sensor reports a state change,
* e.g. from "NEAR to FAR" or from "FAR to NEAR". * e.g. from "NEAR to FAR" or from "FAR to NEAR".
*/ */
private void onProximitySensorChangedState() { private void onProximitySensorChangedState() {
if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) { if (!controlSpeakerByProximitySensor) {
return; return;
} }
@ -274,7 +226,7 @@ public class MagicAudioManager {
// Set initial device states. // Set initial device states.
userSelectedAudioDevice = AudioDevice.NONE; userSelectedAudioDevice = AudioDevice.NONE;
selectedAudioDevice = AudioDevice.NONE; resultingAudioDevice = 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
@ -355,35 +307,10 @@ public class MagicAudioManager {
Log.e(TAG, "Invalid audio device selection"); Log.e(TAG, "Invalid audio device selection");
break; break;
} }
selectedAudioDevice = device; resultingAudioDevice = device;
} }
} }
/**
* Changes default audio device.
* TODO(henrika): add usage of this method in the AppRTCMobile client.
*/
public void setDefaultAudioDevice(AudioDevice defaultDevice) {
ThreadUtils.checkIsOnMainThread();
switch (defaultDevice) {
case SPEAKER_PHONE:
defaultAudioDevice = defaultDevice;
break;
case EARPIECE:
if (hasEarpiece()) {
defaultAudioDevice = defaultDevice;
} else {
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
}
break;
default:
Log.e(TAG, "Invalid default audio device selection");
break;
}
Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
updateAudioDeviceState();
}
/** /**
* Changes selection of the currently active audio device. * Changes selection of the currently active audio device.
*/ */
@ -392,7 +319,15 @@ public class MagicAudioManager {
if (!audioDevices.contains(device)) { if (!audioDevices.contains(device)) {
Log.e(TAG, "Can not select " + device + " from available " + audioDevices); Log.e(TAG, "Can not select " + device + " from available " + audioDevices);
} }
userSelectedAudioDevice = device; userSelectedAudioDevice = device;
if (device == AudioDevice.SPEAKER_PHONE) {
controlSpeakerByProximitySensor = true;
} else {
controlSpeakerByProximitySensor = false;
}
updateAudioDeviceState(); updateAudioDeviceState();
} }
@ -407,9 +342,9 @@ public class MagicAudioManager {
/** /**
* Returns the currently selected audio device. * Returns the currently selected audio device.
*/ */
public AudioDevice getSelectedAudioDevice() { public AudioDevice getResultingAudioDevice() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
return selectedAudioDevice; return resultingAudioDevice;
} }
/** /**
@ -482,10 +417,6 @@ public class MagicAudioManager {
} }
} }
/**
* Updates list of possible audio devices and make new device selection.
* TODO(henrika): add unit test to verify all state transitions.
*/
public void updateAudioDeviceState() { public void updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "--- updateAudioDeviceState: " Log.d(TAG, "--- updateAudioDeviceState: "
@ -493,19 +424,18 @@ public class MagicAudioManager {
+ "BT state=" + bluetoothManager.getState()); + "BT state=" + bluetoothManager.getState());
Log.d(TAG, "Device status: " Log.d(TAG, "Device status: "
+ "available=" + audioDevices + ", " + "available=" + audioDevices + ", "
+ "selected=" + selectedAudioDevice + ", " + "resulting(current)=" + resultingAudioDevice + ", "
+ "user selected=" + userSelectedAudioDevice); + "user selected=" + userSelectedAudioDevice);
// Check if any Bluetooth headset is connected. The internal BT state will
// change accordingly.
// TODO(henrika): perhaps wrap required state into BT manager.
if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE
|| bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE
|| bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) { || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) {
bluetoothManager.updateDevice(); bluetoothManager.updateDevice();
} }
// Update the set of available audio devices.
Set<AudioDevice> newAudioDevices = new HashSet<>(); Set<AudioDevice> newAudioDevices = new HashSet<>();
if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED
@ -518,34 +448,32 @@ public class MagicAudioManager {
// 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(AudioDevice.WIRED_HEADSET);
} else { } else {
// No wired headset, hence the audio-device list can contain speaker
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
newAudioDevices.add(AudioDevice.SPEAKER_PHONE); newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
if (hasEarpiece()) { if (hasEarpiece()) {
newAudioDevices.add(AudioDevice.EARPIECE); newAudioDevices.add(AudioDevice.EARPIECE);
} }
} }
// Store state which is set to true if the device list has changed.
boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices); boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
// Update the existing audio device set.
audioDevices = newAudioDevices; audioDevices = newAudioDevices;
// Correct user selected audio devices if needed. // Correct user selected audio devices if needed.
if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE if (userSelectedAudioDevice == AudioDevice.BLUETOOTH
&& userSelectedAudioDevice == AudioDevice.BLUETOOTH) { && bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE) {
// If BT is not available, it can't be the user selection.
userSelectedAudioDevice = AudioDevice.NONE; userSelectedAudioDevice = AudioDevice.NONE;
} }
if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE && hasWiredHeadset) {
// If user selected speaker phone, but then plugged wired headset then make
// wired headset as user selected device.
userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; userSelectedAudioDevice = AudioDevice.WIRED_HEADSET;
} }
if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { if (userSelectedAudioDevice == AudioDevice.WIRED_HEADSET && !hasWiredHeadset) {
// If user selected wired headset, but then unplugged wired headset then make
// speaker phone as user selected device.
userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; userSelectedAudioDevice = 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 =
@ -586,34 +514,34 @@ public class MagicAudioManager {
// Update selected audio device. // Update selected audio device.
AudioDevice newAudioDevice = selectedAudioDevice; AudioDevice newResultingAudioDevice;
if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { if (bluetoothManager.getState() == MagicBluetoothManager.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; newResultingAudioDevice = 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; newResultingAudioDevice = 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).
// |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE
// depending on the user's selection. // depending on the user's selection.
newAudioDevice = defaultAudioDevice; newResultingAudioDevice = userSelectedAudioDevice;
} }
// Switch to new device but only if there has been any changes. // Switch to new device but only if there has been any changes.
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { if (newResultingAudioDevice != resultingAudioDevice || audioDeviceSetUpdated) {
// Do the required device switch. // Do the required device switch.
setAudioDeviceInternal(newAudioDevice); setAudioDeviceInternal(newResultingAudioDevice);
Log.d(TAG, "New device status: " Log.d(TAG, "New device status: "
+ "available=" + audioDevices + ", " + "available=" + audioDevices + ", "
+ "selected=" + newAudioDevice); + "resulting(new)=" + newResultingAudioDevice);
if (audioManagerEvents != null) { if (audioManagerEvents != null) {
// Notify a listening client that audio device has been changed. // Notify a listening client that audio device has been changed.
audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); audioManagerEvents.onAudioDeviceChanged(resultingAudioDevice, audioDevices);
} }
} }
Log.d(TAG, "--- updateAudioDeviceState done"); Log.d(TAG, "--- updateAudioDeviceState done");