From 9c0fa9acc2489d6d5f3cbe22c704463cd408c219 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 1 Feb 2022 00:42:44 +0100 Subject: [PATCH] select audio device (WIP) Signed-off-by: Marcel Hibbe --- .../talk/activities/CallActivity.java | 47 +++-- .../talk/ui/dialog/AudioOutputDialog.kt | 13 +- .../talk/webrtc/MagicAudioManager.java | 160 +++++------------- 3 files changed, 77 insertions(+), 143 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index f79298f5b..02027eba7 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -32,7 +32,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Color; -import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.AudioAttributes; 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.SignalingSettingsOverall; 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.DisplayUtils; import com.nextcloud.talk.utils.NotificationUtils; @@ -145,6 +143,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; import androidx.core.graphics.drawable.DrawableCompat; import autodagger.AutoInjector; import io.reactivex.Observable; @@ -335,17 +334,6 @@ public class CallActivity extends CallBaseActivity { this ).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.setOnLongClickListener(l -> { if (!microphoneOn) { @@ -381,9 +369,31 @@ public class CallActivity extends CallBaseActivity { }); } - public void setAudioOutputIcon(Drawable drawable){ - binding.audioOutputButton.getHierarchy().setPlaceholderImage(drawable); - DrawableCompat.setTint(drawable, Color.WHITE); + public void setAudioOutputChannel(MagicAudioManager.AudioDevice audioDevice) { + if (audioManager == null) { + 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() { @@ -391,7 +401,7 @@ public class CallActivity extends CallBaseActivity { try { camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(this); } catch (final Throwable throwable) { - Log.w(TAG, "Camera2Enumator threw an error"); + Log.w(TAG, "Camera2Enumerator threw an error"); } if (camera2EnumeratorIsSupported) { @@ -726,7 +736,8 @@ public class CallActivity extends CallBaseActivity { } private void onAudioManagerDevicesChanged( - final MagicAudioManager.AudioDevice device, final Set availableDevices) { + final MagicAudioManager.AudioDevice device, + final Set availableDevices) { Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", " + "selected: " + device); diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt index c8d4cfb2a..eaf999a53 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt @@ -23,7 +23,6 @@ package com.nextcloud.talk.ui.dialog import android.os.Bundle -import android.util.Log import android.view.View import android.view.ViewGroup 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.activities.CallActivity import com.nextcloud.talk.databinding.DialogAudioOutputBinding +import com.nextcloud.talk.webrtc.MagicAudioManager class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(callActivity) { @@ -44,22 +44,17 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call dialogAudioOutputBinding.audioOutputBluetooth.setOnClickListener { - Log.d(TAG, "bluetooth button clicked") - callActivity.setAudioOutputIcon(dialogAudioOutputBinding.audioOutputBluetoothIcon.drawable) + callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.BLUETOOTH) dismiss() } dialogAudioOutputBinding.audioOutputSpeaker.setOnClickListener { - Log.d(TAG, "speaker button clicked") - callActivity.setAudioOutputIcon(dialogAudioOutputBinding.audioOutputSpeakerIcon.drawable) - + callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.SPEAKER_PHONE) dismiss() } dialogAudioOutputBinding.audioOutputEarspeaker.setOnClickListener { - Log.d(TAG, "earspeaker button clicked") - callActivity.setAudioOutputIcon(dialogAudioOutputBinding.audioOutputEarspeakerIcon.drawable) - + callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.EARPIECE) dismiss() } } diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java index 7eba93239..7d640591f 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java @@ -55,13 +55,9 @@ import java.util.Set; */ public class 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; - // Handles all tasks related to Bluetooth headset devices. private final MagicBluetoothManager bluetoothManager; - // Contains speakerphone setting: auto, true or false - private String useSpeakerphone; + private boolean controlSpeakerByProximitySensor; private AudioManager audioManager; private AudioManagerEvents audioManagerEvents; private AudioManagerState amState; @@ -69,31 +65,15 @@ public class MagicAudioManager { private boolean savedIsSpeakerPhoneOn = false; private boolean savedIsMicrophoneMute = 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; - // 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 - // assist device switching (close to ear <=> use headset earpiece if - // available, far from ear <=> use speaker phone). + private AudioDevice resultingAudioDevice; + private MagicProximitySensor proximitySensor = null; - // Contains a list of available audio devices. A Set collection is used to - // avoid duplicate elements. + private Set audioDevices = new HashSet<>(); - // Broadcast receiver for wired headset intent broadcasts. + private BroadcastReceiver wiredHeadsetReceiver; - // Callback method for changes in audio focus. private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; private PowerManagerUtils powerManagerUtils; @@ -110,18 +90,8 @@ public class MagicAudioManager { powerManagerUtils = new PowerManagerUtils(); powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK); - if (useProximitySensor) { - useSpeakerphone = SPEAKERPHONE_AUTO; - } else { - useSpeakerphone = SPEAKERPHONE_FALSE; - } - - - if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) { - defaultAudioDevice = AudioDevice.EARPIECE; - } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; - } + controlSpeakerByProximitySensor = useProximitySensor; + updateAudioDeviceState(); // Create and initialize the proximity sensor. // Tablet devices (e.g. Nexus 7) does not support proximity sensors. @@ -134,8 +104,6 @@ public class MagicAudioManager { onProximitySensorChangedState(); } }); - - Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice); } /** @@ -145,29 +113,13 @@ public class MagicAudioManager { 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, * e.g. from "NEAR to FAR" or from "FAR to NEAR". */ private void onProximitySensorChangedState() { - if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) { + if (!controlSpeakerByProximitySensor) { return; } @@ -274,7 +226,7 @@ public class MagicAudioManager { // Set initial device states. userSelectedAudioDevice = AudioDevice.NONE; - selectedAudioDevice = AudioDevice.NONE; + resultingAudioDevice = AudioDevice.NONE; audioDevices.clear(); // 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"); 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. */ @@ -392,7 +319,15 @@ public class MagicAudioManager { if (!audioDevices.contains(device)) { Log.e(TAG, "Can not select " + device + " from available " + audioDevices); } + userSelectedAudioDevice = device; + + if (device == AudioDevice.SPEAKER_PHONE) { + controlSpeakerByProximitySensor = true; + } else { + controlSpeakerByProximitySensor = false; + } + updateAudioDeviceState(); } @@ -407,9 +342,9 @@ public class MagicAudioManager { /** * Returns the currently selected audio device. */ - public AudioDevice getSelectedAudioDevice() { + public AudioDevice getResultingAudioDevice() { 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() { ThreadUtils.checkIsOnMainThread(); Log.d(TAG, "--- updateAudioDeviceState: " @@ -493,19 +424,18 @@ public class MagicAudioManager { + "BT state=" + bluetoothManager.getState()); Log.d(TAG, "Device status: " + "available=" + audioDevices + ", " - + "selected=" + selectedAudioDevice + ", " + + "resulting(current)=" + resultingAudioDevice + ", " + "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 || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) { bluetoothManager.updateDevice(); } - // Update the set of available audio devices. Set newAudioDevices = new HashSet<>(); 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. newAudioDevices.add(AudioDevice.WIRED_HEADSET); } 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); if (hasEarpiece()) { newAudioDevices.add(AudioDevice.EARPIECE); } } - // Store state which is set to true if the device list has changed. + boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices); - // Update the existing audio device set. audioDevices = newAudioDevices; + + + // Correct user selected audio devices if needed. - if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE - && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { - // If BT is not available, it can't be the user selection. + if (userSelectedAudioDevice == AudioDevice.BLUETOOTH + && bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE) { userSelectedAudioDevice = AudioDevice.NONE; } - if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { - // If user selected speaker phone, but then plugged wired headset then make - // wired headset as user selected device. + if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE && hasWiredHeadset) { userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; } - if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { - // If user selected wired headset, but then unplugged wired headset then make - // speaker phone as user selected device. + if (userSelectedAudioDevice == AudioDevice.WIRED_HEADSET && !hasWiredHeadset) { userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; } + + + // Need to start Bluetooth if it is available and user either selected it explicitly or // user did not select any output device. boolean needBluetoothAudioStart = @@ -586,34 +514,34 @@ public class MagicAudioManager { // Update selected audio device. - AudioDevice newAudioDevice = selectedAudioDevice; + AudioDevice newResultingAudioDevice; if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { // 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; // an active SCO channel must also be up and running. - newAudioDevice = AudioDevice.BLUETOOTH; + newResultingAudioDevice = AudioDevice.BLUETOOTH; } else if (hasWiredHeadset) { // If a wired headset is connected, but Bluetooth is not, then wired headset is used as // audio device. - newAudioDevice = AudioDevice.WIRED_HEADSET; + newResultingAudioDevice = AudioDevice.WIRED_HEADSET; } else { // 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). // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE // depending on the user's selection. - newAudioDevice = defaultAudioDevice; + newResultingAudioDevice = userSelectedAudioDevice; } // 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. - setAudioDeviceInternal(newAudioDevice); + setAudioDeviceInternal(newResultingAudioDevice); Log.d(TAG, "New device status: " + "available=" + audioDevices + ", " - + "selected=" + newAudioDevice); + + "resulting(new)=" + newResultingAudioDevice); if (audioManagerEvents != null) { // Notify a listening client that audio device has been changed. - audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); + audioManagerEvents.onAudioDeviceChanged(resultingAudioDevice, audioDevices); } } Log.d(TAG, "--- updateAudioDeviceState done");