浏览代码

Merge pull request #1794 from nextcloud/feature/1761/selectAudioOutput

Feature/1761/select audio output
Marcel Hibbe 3 年之前
父节点
当前提交
f076a81427

+ 65 - 23
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -82,6 +82,7 @@ import com.nextcloud.talk.models.json.signaling.Signaling;
 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.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.NotificationUtils;
@@ -142,6 +143,8 @@ 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;
 import io.reactivex.Observer;
@@ -170,6 +173,8 @@ public class CallActivity extends CallBaseActivity {
 
     public static final String TAG = "CallActivity";
 
+    public MagicAudioManager audioManager;
+
     private static final String[] PERMISSIONS_CALL = {
         android.Manifest.permission.CAMERA,
         android.Manifest.permission.RECORD_AUDIO,
@@ -195,7 +200,6 @@ public class CallActivity extends CallBaseActivity {
     private MediaConstraints videoConstraints;
     private MediaConstraints sdpConstraints;
     private MediaConstraints sdpConstraintsForMCU;
-    private MagicAudioManager audioManager;
     private VideoSource videoSource;
     private VideoTrack localVideoTrack;
     private AudioSource audioSource;
@@ -252,6 +256,8 @@ public class CallActivity extends CallBaseActivity {
 
     private CallActivityBinding binding;
 
+    private AudioOutputDialog audioOutputDialog;
+
     @Parcel
     public enum CallStatus {
         CONNECTING, CALLING_TIMEOUT, JOINED, IN_CONVERSATION, RECONNECTING, OFFLINE, LEAVING, PUBLISHER_FAILED
@@ -327,15 +333,9 @@ public class CallActivity extends CallBaseActivity {
     private void initClickListeners() {
         binding.pictureInPictureButton.setOnClickListener(l -> enterPipMode());
 
-        binding.speakerButton.setOnClickListener(l -> {
-            if (audioManager != null) {
-                audioManager.toggleUseSpeakerphone();
-                if (audioManager.isSpeakerphoneAutoOn()) {
-                    binding.speakerButton.getHierarchy().setPlaceholderImage(R.drawable.ic_volume_up_white_24dp);
-                } else {
-                    binding.speakerButton.getHierarchy().setPlaceholderImage(R.drawable.ic_volume_mute_white_24dp);
-                }
-            }
+        binding.audioOutputButton.setOnClickListener(v -> {
+            audioOutputDialog = new AudioOutputDialog(this);
+            audioOutputDialog.show();
         });
 
         binding.microphoneButton.setOnClickListener(l -> onMicrophoneClick());
@@ -377,8 +377,8 @@ public class CallActivity extends CallBaseActivity {
         boolean camera2EnumeratorIsSupported = false;
         try {
             camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(this);
-        } catch (final Throwable throwable) {
-            Log.w(TAG, "Camera2Enumator threw an error");
+        } catch (final Throwable t) {
+            Log.w(TAG, "Camera2Enumerator threw an error", t);
         }
 
         if (camera2EnumeratorIsSupported) {
@@ -412,12 +412,18 @@ public class CallActivity extends CallBaseActivity {
 
         // Create and audio manager that will take care of audio routing,
         // audio modes, audio device enumeration etc.
-        audioManager = MagicAudioManager.create(getApplicationContext(), !isVoiceOnlyCall);
+        audioManager = MagicAudioManager.create(getApplicationContext(), isVoiceOnlyCall);
         // Store existing audio settings and change audio mode to
         // MODE_IN_COMMUNICATION for best possible VoIP performance.
         Log.d(TAG, "Starting the audio manager...");
         audioManager.start(this::onAudioManagerDevicesChanged);
 
+        if (isVoiceOnlyCall) {
+            setAudioOutputChannel(MagicAudioManager.AudioDevice.EARPIECE);
+        } else {
+            setAudioOutputChannel(MagicAudioManager.AudioDevice.SPEAKER_PHONE);
+        }
+
         iceServers = new ArrayList<>();
 
         //create sdpConstraints
@@ -448,6 +454,38 @@ public class CallActivity extends CallBaseActivity {
         microphoneInitialization();
     }
 
+    public void setAudioOutputChannel(MagicAudioManager.AudioDevice selectedAudioDevice) {
+        if (audioManager != null) {
+            audioManager.selectAudioDevice(selectedAudioDevice);
+            updateAudioOutputButton(audioManager.getCurrentAudioDevice());
+        }
+    }
+
+    private void updateAudioOutputButton(MagicAudioManager.AudioDevice activeAudioDevice) {
+        switch (activeAudioDevice) {
+            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;
+            case WIRED_HEADSET:
+                binding.audioOutputButton.getHierarchy().setPlaceholderImage(
+                    AppCompatResources.getDrawable(context, R.drawable.ic_baseline_headset_mic_24));
+                break;
+            default:
+                Log.e(TAG, "Icon for audio output not available");
+                break;
+        }
+        DrawableCompat.setTint(binding.audioOutputButton.getDrawable(), Color.WHITE);
+    }
+
     private void handleFromNotification() {
         int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
 
@@ -496,7 +534,6 @@ public class CallActivity extends CallBaseActivity {
         }
 
         if (isVoiceOnlyCall) {
-            binding.speakerButton.setVisibility(View.VISIBLE);
             binding.switchSelfVideoButton.setVisibility(View.GONE);
             binding.cameraButton.setVisibility(View.GONE);
             binding.selfVideoRenderer.setVisibility(View.GONE);
@@ -513,7 +550,6 @@ public class CallActivity extends CallBaseActivity {
             params.setMargins(0, 0, 0, 0);
             binding.gridview.setLayoutParams(params);
 
-            binding.speakerButton.setVisibility(View.GONE);
             if (cameraEnumerator.getDeviceNames().length < 2) {
                 binding.switchSelfVideoButton.setVisibility(View.GONE);
             }
@@ -713,19 +749,25 @@ public class CallActivity extends CallBaseActivity {
     }
 
     private void onAudioManagerDevicesChanged(
-        final MagicAudioManager.AudioDevice device, final Set<MagicAudioManager.AudioDevice> availableDevices) {
+        final MagicAudioManager.AudioDevice currentDevice,
+        final Set<MagicAudioManager.AudioDevice> availableDevices) {
         Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
-            + "selected: " + device);
+            + "currentDevice: " + currentDevice);
 
-        final boolean shouldDisableProximityLock = (device.equals(MagicAudioManager.AudioDevice.WIRED_HEADSET)
-            || device.equals(MagicAudioManager.AudioDevice.SPEAKER_PHONE)
-            || device.equals(MagicAudioManager.AudioDevice.BLUETOOTH));
+        final boolean shouldDisableProximityLock = (currentDevice.equals(MagicAudioManager.AudioDevice.WIRED_HEADSET)
+            || currentDevice.equals(MagicAudioManager.AudioDevice.SPEAKER_PHONE)
+            || currentDevice.equals(MagicAudioManager.AudioDevice.BLUETOOTH));
 
         if (shouldDisableProximityLock) {
             powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK);
         } else {
             powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK);
         }
+
+        if (audioOutputDialog != null) {
+            audioOutputDialog.updateOutputDeviceList();
+        }
+        updateAudioOutputButton(currentDevice);
     }
 
 
@@ -1641,10 +1683,10 @@ public class CallActivity extends CallBaseActivity {
         Log.d(TAG, "   currentSessionId is " + currentSessionId);
 
         for (HashMap<String, Object> participant : users) {
-            long inCallFlag = (long)participant.get("inCall");
+            long inCallFlag = (long) participant.get("inCall");
             if (!participant.get("sessionId").equals(currentSessionId)) {
                 boolean isNewSession;
-                Log.d(TAG, "   inCallFlag of participant " + participant.get("sessionId").toString().substring(0,4) + " : " + inCallFlag);
+                Log.d(TAG, "   inCallFlag of participant " + participant.get("sessionId").toString().substring(0, 4) + " : " + inCallFlag);
                 isNewSession = inCallFlag != 0;
 
                 if (isNewSession) {
@@ -1654,7 +1696,7 @@ public class CallActivity extends CallBaseActivity {
                 }
             } else {
                 Log.d(TAG, "   inCallFlag of currentSessionId: " + inCallFlag);
-                if (inCallFlag == 0){
+                if (inCallFlag == 0) {
                     Log.d(TAG, "Most probably a moderator ended the call for all.");
                     hangup(true);
                 }

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java

@@ -50,7 +50,7 @@ public class MenuItem extends AbstractFlexibleItem<MenuItem.MenuItemViewHolder>
         this.title = title;
         this.tag = tag;
         this.icon = icon;
-        padding = (int) DisplayUtils.convertDpToPixel(16,
+        padding = (int) DisplayUtils.convertDpToPixel(32,
                 NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext());
     }
 

+ 167 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt

@@ -0,0 +1,167 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.ui.dialog
+
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+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) {
+
+    private lateinit var dialogAudioOutputBinding: DialogAudioOutputBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        dialogAudioOutputBinding = DialogAudioOutputBinding.inflate(layoutInflater)
+        setContentView(dialogAudioOutputBinding.root)
+        window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+
+        updateOutputDeviceList()
+        initClickListeners()
+    }
+
+    fun updateOutputDeviceList() {
+        if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.BLUETOOTH) == false) {
+            dialogAudioOutputBinding.audioOutputBluetooth.visibility = View.GONE
+        } else {
+            dialogAudioOutputBinding.audioOutputBluetooth.visibility = View.VISIBLE
+        }
+
+        if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.EARPIECE) == false) {
+            dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.GONE
+        } else {
+            dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.VISIBLE
+        }
+
+        if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.SPEAKER_PHONE) == false) {
+            dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.GONE
+        } else {
+            dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.VISIBLE
+        }
+
+        if (callActivity.audioManager?.currentAudioDevice?.equals(
+                MagicAudioManager.AudioDevice.WIRED_HEADSET
+            ) == true
+        ) {
+            dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.GONE
+            dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.GONE
+            dialogAudioOutputBinding.audioOutputWiredHeadset.visibility = View.VISIBLE
+        } else {
+            dialogAudioOutputBinding.audioOutputWiredHeadset.visibility = View.GONE
+        }
+
+        highlightActiveOutputChannel()
+    }
+
+    private fun highlightActiveOutputChannel() {
+        when (callActivity.audioManager?.currentAudioDevice) {
+            MagicAudioManager.AudioDevice.BLUETOOTH -> {
+                dialogAudioOutputBinding.audioOutputBluetoothIcon.setColorFilter(
+                    ContextCompat.getColor(
+                        context,
+                        R.color.colorPrimary
+                    ),
+                    android.graphics.PorterDuff.Mode.SRC_IN
+                )
+                dialogAudioOutputBinding.audioOutputBluetoothText.setTextColor(
+                    callActivity.resources.getColor(R.color.colorPrimary)
+                )
+            }
+
+            MagicAudioManager.AudioDevice.SPEAKER_PHONE -> {
+                dialogAudioOutputBinding.audioOutputSpeakerIcon.setColorFilter(
+                    ContextCompat.getColor(
+                        context,
+                        R.color.colorPrimary
+                    ),
+                    android.graphics.PorterDuff.Mode.SRC_IN
+                )
+                dialogAudioOutputBinding.audioOutputSpeakerText.setTextColor(
+                    callActivity.resources.getColor(R.color.colorPrimary)
+                )
+            }
+
+            MagicAudioManager.AudioDevice.EARPIECE -> {
+                dialogAudioOutputBinding.audioOutputEarspeakerIcon.setColorFilter(
+                    ContextCompat.getColor(
+                        context,
+                        R.color.colorPrimary
+                    ),
+                    android.graphics.PorterDuff.Mode.SRC_IN
+                )
+                dialogAudioOutputBinding.audioOutputEarspeakerText.setTextColor(
+                    callActivity.resources.getColor(R.color.colorPrimary)
+                )
+            }
+
+            MagicAudioManager.AudioDevice.WIRED_HEADSET -> {
+                dialogAudioOutputBinding.audioOutputWiredHeadsetIcon.setColorFilter(
+                    ContextCompat.getColor(
+                        context,
+                        R.color.colorPrimary
+                    ),
+                    android.graphics.PorterDuff.Mode.SRC_IN
+                )
+                dialogAudioOutputBinding.audioOutputWiredHeadsetText.setTextColor(
+                    callActivity.resources.getColor(R.color.colorPrimary)
+                )
+            }
+
+            else -> Log.d(TAG, "AudioOutputDialog doesn't know this AudioDevice")
+        }
+    }
+
+    private fun initClickListeners() {
+        dialogAudioOutputBinding.audioOutputBluetooth.setOnClickListener {
+            callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.BLUETOOTH)
+            dismiss()
+        }
+
+        dialogAudioOutputBinding.audioOutputSpeaker.setOnClickListener {
+            callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.SPEAKER_PHONE)
+            dismiss()
+        }
+
+        dialogAudioOutputBinding.audioOutputEarspeaker.setOnClickListener {
+            callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.EARPIECE)
+            dismiss()
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        val bottomSheet = findViewById<View>(R.id.design_bottom_sheet)
+        val behavior = BottomSheetBehavior.from(bottomSheet as View)
+        behavior.state = BottomSheetBehavior.STATE_EXPANDED
+    }
+
+    companion object {
+        private const val TAG = "AudioOutputDialog"
+    }
+}

+ 82 - 177
app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java

@@ -41,8 +41,10 @@ import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.os.Build;
 import android.util.Log;
+
 import com.nextcloud.talk.events.PeerConnectionEvent;
 import com.nextcloud.talk.utils.power.PowerManagerUtils;
+
 import org.greenrobot.eventbus.EventBus;
 import org.webrtc.ThreadUtils;
 
@@ -55,45 +57,25 @@ 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 useProximitySensor;
     private AudioManager audioManager;
-    private AudioManagerEvents audioManagerEvents;
+    private AudioManagerListener audioManagerListener;
     private AudioManagerState amState;
     private int savedAudioMode = AudioManager.MODE_INVALID;
     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 currentAudioDevice;
+
     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<>();
-    // 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 +92,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;
-        }
+        this.useProximitySensor = useProximitySensor;
+        updateAudioDeviceState();
 
         // Create and initialize the proximity sensor.
         // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
@@ -134,8 +106,6 @@ public class MagicAudioManager {
                 onProximitySensorChangedState();
             }
         });
-
-        Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice);
     }
 
     /**
@@ -145,57 +115,38 @@ 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".
+     * 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 (!useProximitySensor) {
             return;
         }
 
-        // The proximity sensor should only be activated when there are exactly two
-        // available audio devices.
-        if (audioDevices.size() == 2 && audioDevices.contains(MagicAudioManager.AudioDevice.EARPIECE)
-                && audioDevices.contains(MagicAudioManager.AudioDevice.SPEAKER_PHONE)) {
+        if (userSelectedAudioDevice.equals(AudioDevice.SPEAKER_PHONE)
+            && audioDevices.contains(AudioDevice.EARPIECE)
+            && audioDevices.contains(AudioDevice.SPEAKER_PHONE)) {
+
             if (proximitySensor.sensorReportsNearState()) {
-                // Sensor reports that a "handset is being held up to a person's ear",
-                // or "something is covering the light sensor".
-                setAudioDeviceInternal(MagicAudioManager.AudioDevice.EARPIECE);
+                setAudioDeviceInternal(AudioDevice.EARPIECE);
+                Log.d(TAG, "switched to EARPIECE because userSelectedAudioDevice was SPEAKER_PHONE and proximity=near");
 
                 EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
-                        .SENSOR_NEAR, null, null, null, null));
+                                                                       .SENSOR_NEAR, null, null, null, null));
 
             } else {
-                // Sensor reports that a "handset is removed from a person's ear", or
-                // "the light sensor is no longer covered".
                 setAudioDeviceInternal(MagicAudioManager.AudioDevice.SPEAKER_PHONE);
+                Log.d(TAG, "switched to SPEAKER_PHONE because userSelectedAudioDevice was SPEAKER_PHONE and proximity=far");
 
                 EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
-                        .SENSOR_FAR, null, null, null, null));
+                                                                       .SENSOR_FAR, null, null, null, null));
             }
         }
     }
 
     @SuppressLint("WrongConstant")
-    public void start(AudioManagerEvents audioManagerEvents) {
+    public void start(AudioManagerListener audioManagerListener) {
         Log.d(TAG, "start");
         ThreadUtils.checkIsOnMainThread();
         if (amState == AudioManagerState.RUNNING) {
@@ -205,7 +156,7 @@ public class MagicAudioManager {
         // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED.
 
         Log.d(TAG, "AudioManager starts...");
-        this.audioManagerEvents = audioManagerEvents;
+        this.audioManagerListener = audioManagerListener;
         amState = AudioManagerState.RUNNING;
 
         // Store current audio state so we can restore it when stop() is called.
@@ -257,7 +208,7 @@ public class MagicAudioManager {
 
         // Request audio playout focus (without ducking) and install listener for changes in focus.
         int result = audioManager.requestAudioFocus(audioFocusChangeListener,
-                AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+                                                    AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
         if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
             Log.d(TAG, "Audio focus request granted for VOICE_CALL streams");
         } else {
@@ -274,7 +225,7 @@ public class MagicAudioManager {
 
         // Set initial device states.
         userSelectedAudioDevice = AudioDevice.NONE;
-        selectedAudioDevice = AudioDevice.NONE;
+        currentAudioDevice = AudioDevice.NONE;
         audioDevices.clear();
 
         // Initialize and start Bluetooth if a BT device is available or initiate
@@ -324,7 +275,7 @@ public class MagicAudioManager {
 
         powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE);
 
-        audioManagerEvents = null;
+        audioManagerListener = null;
         Log.d(TAG, "AudioManager stopped");
     }
 
@@ -333,21 +284,16 @@ public class MagicAudioManager {
     /**
      * Changes selection of the currently active audio device.
      */
-    private void setAudioDeviceInternal(AudioDevice device) {
-        Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")");
-
-        if (audioDevices.contains(device)) {
+    private void setAudioDeviceInternal(AudioDevice audioDevice) {
+        Log.d(TAG, "setAudioDeviceInternal(device=" + audioDevice + ")");
 
-            switch (device) {
+        if (audioDevices.contains(audioDevice)) {
+            switch (audioDevice) {
                 case SPEAKER_PHONE:
                     setSpeakerphoneOn(true);
                     break;
                 case EARPIECE:
-                    setSpeakerphoneOn(false);
-                    break;
                 case WIRED_HEADSET:
-                    setSpeakerphoneOn(false);
-                    break;
                 case BLUETOOTH:
                     setSpeakerphoneOn(false);
                     break;
@@ -355,33 +301,8 @@ public class MagicAudioManager {
                     Log.e(TAG, "Invalid audio device selection");
                     break;
             }
-            selectedAudioDevice = 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;
+            currentAudioDevice = audioDevice;
         }
-        Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
-        updateAudioDeviceState();
     }
 
     /**
@@ -407,9 +328,9 @@ public class MagicAudioManager {
     /**
      * Returns the currently selected audio device.
      */
-    public AudioDevice getSelectedAudioDevice() {
+    public AudioDevice getCurrentAudioDevice() {
         ThreadUtils.checkIsOnMainThread();
-        return selectedAudioDevice;
+        return currentAudioDevice;
     }
 
     /**
@@ -456,11 +377,9 @@ public class MagicAudioManager {
     }
 
     /**
-     * Checks whether a wired headset is connected or not.
-     * This is not a valid indication that audio playback is actually over
-     * the wired headset as audio routing depends on other conditions. We
-     * only use it as an early indicator (during initialization) of an attached
-     * wired headset.
+     * Checks whether a wired headset is connected or not. This is not a valid indication that audio playback is
+     * actually over the wired headset as audio routing depends on other conditions. We only use it as an early
+     * indicator (during initialization) of an attached wired headset.
      */
     @Deprecated
     private boolean hasWiredHeadset() {
@@ -482,35 +401,27 @@ 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: "
-                + "wired headset=" + hasWiredHeadset + ", "
-                + "BT state=" + bluetoothManager.getState());
+            + "wired headset=" + hasWiredHeadset + ", "
+            + "BT state=" + bluetoothManager.getState());
         Log.d(TAG, "Device status: "
-                + "available=" + audioDevices + ", "
-                + "selected=" + selectedAudioDevice + ", "
-                + "user selected=" + userSelectedAudioDevice);
+            + "available=" + audioDevices + ", "
+            + "current=" + currentAudioDevice + ", "
+            + "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.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE
+            || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) {
             bluetoothManager.updateDevice();
         }
 
-        // Update the set of available audio devices.
         Set<AudioDevice> newAudioDevices = new HashSet<>();
 
         if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED
-                || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING
-                || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE) {
+            || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING
+            || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE) {
             newAudioDevices.add(AudioDevice.BLUETOOTH);
         }
 
@@ -518,55 +429,50 @@ 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.
-            userSelectedAudioDevice = AudioDevice.NONE;
+        if (userSelectedAudioDevice == AudioDevice.BLUETOOTH
+            && bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE) {
+            userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE;
         }
-        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 =
-                bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE
-                        && (userSelectedAudioDevice == AudioDevice.NONE
-                        || userSelectedAudioDevice == AudioDevice.BLUETOOTH);
+            bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE
+                && (userSelectedAudioDevice == AudioDevice.NONE
+                || userSelectedAudioDevice == AudioDevice.BLUETOOTH);
 
         // Need to stop Bluetooth audio if user selected different device and
         // Bluetooth SCO connection is established or in the process.
         boolean needBluetoothAudioStop =
-                (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED
-                        || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING)
-                        && (userSelectedAudioDevice != AudioDevice.NONE
-                        && userSelectedAudioDevice != AudioDevice.BLUETOOTH);
+            (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED
+                || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING)
+                && (userSelectedAudioDevice != AudioDevice.NONE
+                && userSelectedAudioDevice != AudioDevice.BLUETOOTH);
 
         if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE
-                || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING
-                || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) {
+            || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING
+            || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) {
             Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
-                    + "stop=" + needBluetoothAudioStop + ", "
-                    + "BT state=" + bluetoothManager.getState());
+                + "stop=" + needBluetoothAudioStop + ", "
+                + "BT state=" + bluetoothManager.getState());
         }
 
         // Start or stop Bluetooth SCO connection given states set earlier.
@@ -577,8 +483,8 @@ public class MagicAudioManager {
 
         // Attempt to start Bluetooth SCO audio (takes a few second to start).
         if (needBluetoothAudioStart &&
-                !needBluetoothAudioStop &&
-                !bluetoothManager.startScoAudio()) {
+            !needBluetoothAudioStop &&
+            !bluetoothManager.startScoAudio()) {
             // Remove BLUETOOTH from list of available devices since SCO failed.
             audioDevices.remove(AudioDevice.BLUETOOTH);
             audioDeviceSetUpdated = true;
@@ -586,42 +492,41 @@ public class MagicAudioManager {
 
 
         // Update selected audio device.
-        AudioDevice newAudioDevice = selectedAudioDevice;
+        AudioDevice newCurrentAudioDevice;
 
         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;
+            newCurrentAudioDevice = 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;
+            newCurrentAudioDevice = 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;
+            newCurrentAudioDevice = userSelectedAudioDevice;
         }
         // Switch to new device but only if there has been any changes.
-        if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
+        if (newCurrentAudioDevice != currentAudioDevice || audioDeviceSetUpdated) {
             // Do the required device switch.
-            setAudioDeviceInternal(newAudioDevice);
+            setAudioDeviceInternal(newCurrentAudioDevice);
             Log.d(TAG, "New device status: "
-                    + "available=" + audioDevices + ", "
-                    + "selected=" + newAudioDevice);
-            if (audioManagerEvents != null) {
+                + "available=" + audioDevices + ", "
+                + "current(new)=" + newCurrentAudioDevice);
+            if (audioManagerListener != null) {
                 // Notify a listening client that audio device has been changed.
-                audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
+                audioManagerListener.onAudioDeviceChanged(currentAudioDevice, audioDevices);
             }
         }
         Log.d(TAG, "--- updateAudioDeviceState done");
     }
 
     /**
-     * AudioDevice is the names of possible audio devices that we currently
-     * support.
+     * AudioDevice is the names of possible audio devices that we currently support.
      */
     public enum AudioDevice {
         SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
@@ -639,10 +544,10 @@ public class MagicAudioManager {
     /**
      * Selected audio device change event.
      */
-    public static interface AudioManagerEvents {
+    public static interface AudioManagerListener {
         // Callback fired once audio device is changed or list of available audio devices changed.
         void onAudioDeviceChanged(
-                AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
+            AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
     }
 
     /* Receiver which handles changes in wired headset availability. */

+ 0 - 26
app/src/main/res/drawable/ic_baseline_attachment_24.xml

@@ -1,26 +0,0 @@
-<!--
-    @author Google LLC
-    Copyright (C) 2021 Google LLC
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
-  <path
-      android:fillColor="@android:color/white"
-      android:pathData="M2,12.5C2,9.46 4.46,7 7.5,7H18c2.21,0 4,1.79 4,4s-1.79,4 -4,4H9.5C8.12,15 7,13.88 7,12.5S8.12,10 9.5,10H17v2H9.41c-0.55,0 -0.55,1 0,1H18c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2H7.5C5.57,9 4,10.57 4,12.5S5.57,16 7.5,16H17v2H7.5C4.46,18 2,15.54 2,12.5z"/>
-</vector>

+ 10 - 0
app/src/main/res/drawable/ic_baseline_bluetooth_audio_24.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M14.24,12.01l2.32,2.32c0.28,-0.72 0.44,-1.51 0.44,-2.33 0,-0.82 -0.16,-1.59 -0.43,-2.31l-2.33,2.32zM19.53,6.71l-1.26,1.26c0.63,1.21 0.98,2.57 0.98,4.02s-0.36,2.82 -0.98,4.02l1.2,1.2c0.97,-1.54 1.54,-3.36 1.54,-5.31 -0.01,-1.89 -0.55,-3.67 -1.48,-5.19zM15.71,7.71L10,2L9,2v7.59L4.41,5 3,6.41 8.59,12 3,17.59 4.41,19 9,14.41L9,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM11,5.83l1.88,1.88L11,9.59L11,5.83zM12.88,16.29L11,18.17v-3.76l1.88,1.88z"/>
+</vector>

+ 10 - 0
app/src/main/res/drawable/ic_baseline_headset_mic_24.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h4v1h-7v2h6c1.66,0 3,-1.34 3,-3V10c0,-4.97 -4.03,-9 -9,-9z"/>
+</vector>

+ 10 - 0
app/src/main/res/drawable/ic_baseline_phone_in_talk_24.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M20,15.5c-1.25,0 -2.45,-0.2 -3.57,-0.57 -0.35,-0.11 -0.74,-0.03 -1.02,0.24l-2.2,2.2c-2.83,-1.44 -5.15,-3.75 -6.59,-6.59l2.2,-2.21c0.28,-0.26 0.36,-0.65 0.25,-1C8.7,6.45 8.5,5.25 8.5,4c0,-0.55 -0.45,-1 -1,-1L4,3c-0.55,0 -1,0.45 -1,1 0,9.39 7.61,17 17,17 0.55,0 1,-0.45 1,-1v-3.5c0,-0.55 -0.45,-1 -1,-1zM19,12h2c0,-4.97 -4.03,-9 -9,-9v2c3.87,0 7,3.13 7,7zM15,12h2c0,-2.76 -2.24,-5 -5,-5v2c1.66,0 3,1.34 3,3z"/>
+</vector>

+ 20 - 13
app/src/main/res/layout/call_activity.xml

@@ -140,59 +140,66 @@
         android:animateLayoutChanges="true"
         android:background="@android:color/transparent"
         android:gravity="center"
-        android:orientation="horizontal">
+        android:orientation="horizontal"
+        android:weightSum="5">
 
         <com.facebook.drawee.view.SimpleDraweeView
             android:id="@+id/pictureInPictureButton"
-            android:layout_width="60dp"
+            android:layout_width="0dp"
             android:layout_height="match_parent"
+            android:layout_marginStart="20dp"
             android:layout_marginEnd="10dp"
             android:elevation="10dp"
             app:backgroundImage="@color/call_buttons_background"
             app:placeholderImage="@drawable/ic_baseline_picture_in_picture_alt_24"
-            app:roundAsCircle="true" />
+            app:roundAsCircle="true"
+            android:layout_weight="1"/>
 
         <com.facebook.drawee.view.SimpleDraweeView
-            android:id="@+id/speakerButton"
-            android:layout_width="60dp"
+            android:id="@+id/audioOutputButton"
+            android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_marginStart="10dp"
             android:layout_marginEnd="10dp"
             app:backgroundImage="@color/call_buttons_background"
             app:placeholderImage="@drawable/ic_volume_mute_white_24dp"
-            app:roundAsCircle="true" />
+            app:roundAsCircle="true"
+            android:layout_weight="1"/>
 
         <com.facebook.drawee.view.SimpleDraweeView
             android:id="@+id/cameraButton"
-            android:layout_width="60dp"
+            android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_marginStart="10dp"
             android:layout_marginEnd="10dp"
             android:alpha="0.7"
             app:backgroundImage="@color/call_buttons_background"
             app:placeholderImage="@drawable/ic_videocam_white_24px"
-            app:roundAsCircle="true" />
+            app:roundAsCircle="true"
+            android:layout_weight="1"/>
 
         <com.facebook.drawee.view.SimpleDraweeView
             android:id="@+id/microphoneButton"
-            android:layout_width="60dp"
+            android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_marginStart="10dp"
             android:layout_marginEnd="10dp"
             android:alpha="0.7"
             app:backgroundImage="@color/call_buttons_background"
             app:placeholderImage="@drawable/ic_mic_off_white_24px"
-            app:roundAsCircle="true" />
+            app:roundAsCircle="true"
+            android:layout_weight="1"/>
 
         <com.facebook.drawee.view.SimpleDraweeView
             android:id="@+id/hangupButton"
-            android:layout_width="60dp"
+            android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_marginStart="10dp"
-            android:layout_marginEnd="10dp"
+            android:layout_marginEnd="20dp"
             app:backgroundImage="@color/nc_darkRed"
             app:placeholderImage="@drawable/ic_call_end_white_24px"
-            app:roundAsCircle="true" />
+            app:roundAsCircle="true"
+            android:layout_weight="1"/>
     </LinearLayout>
 
     <LinearLayout

+ 25 - 33
app/src/main/res/layout/dialog_attachment.xml

@@ -27,13 +27,15 @@
     android:layout_height="wrap_content"
     android:background="@color/bg_bottom_sheet"
     android:orientation="vertical"
-    android:paddingBottom="@dimen/standard_padding">
+    android:paddingStart="@dimen/standard_padding"
+    android:paddingEnd="@dimen/standard_padding"
+    android:paddingBottom="@dimen/standard_half_padding">
 
     <TextView
         android:id="@+id/upload"
         android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:padding="@dimen/standard_padding"
+        android:layout_height="@dimen/bottom_sheet_item_height"
+        android:gravity="start|center_vertical"
         android:text="@string/nc_add_file"
         android:textAlignment="viewStart"
         android:textColor="@color/medium_emphasis_text"
@@ -42,13 +44,10 @@
     <LinearLayout
         android:id="@+id/menu_attach_contact"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="@dimen/bottom_sheet_item_height"
         android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
         android:orientation="horizontal"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_half_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingBottom="@dimen/standard_half_padding"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -64,7 +63,8 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="start|center_vertical"
-            android:layout_marginStart="@dimen/standard_margin"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
             android:text="@string/nc_share_contact"
             android:textAlignment="viewStart"
             android:textColor="@color/high_emphasis_text"
@@ -75,13 +75,10 @@
     <LinearLayout
         android:id="@+id/menu_share_location"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="@dimen/bottom_sheet_item_height"
         android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
         android:orientation="horizontal"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_half_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingBottom="@dimen/standard_half_padding"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -97,7 +94,8 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="start|center_vertical"
-            android:layout_marginStart="@dimen/standard_margin"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
             android:text="@string/nc_share_location"
             android:textAlignment="viewStart"
             android:textColor="@color/high_emphasis_text"
@@ -108,13 +106,10 @@
     <LinearLayout
         android:id="@+id/menu_attach_picture_from_cam"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="@dimen/bottom_sheet_item_height"
         android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
         android:orientation="horizontal"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_half_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingBottom="@dimen/standard_half_padding"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -130,7 +125,8 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="start|center_vertical"
-            android:layout_marginStart="@dimen/standard_margin"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
             android:text="@string/nc_upload_picture_from_cam"
             android:textAlignment="viewStart"
             android:textColor="@color/high_emphasis_text"
@@ -141,13 +137,10 @@
     <LinearLayout
         android:id="@+id/menu_attach_file_from_local"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="@dimen/bottom_sheet_item_height"
         android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
         android:orientation="horizontal"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_half_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingBottom="@dimen/standard_half_padding"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -163,7 +156,8 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="start|center_vertical"
-            android:layout_marginStart="@dimen/standard_margin"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
             android:text="@string/nc_upload_local_file"
             android:textAlignment="viewStart"
             android:textColor="@color/high_emphasis_text"
@@ -174,13 +168,10 @@
     <LinearLayout
         android:id="@+id/menu_attach_file_from_cloud"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="@dimen/bottom_sheet_item_height"
         android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
         android:orientation="horizontal"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_half_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingBottom="@dimen/standard_padding"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -196,7 +187,8 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="start|center_vertical"
-            android:layout_marginStart="@dimen/standard_margin"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
             android:textAlignment="viewStart"
             android:textColor="@color/high_emphasis_text"
             android:textSize="@dimen/bottom_sheet_text_size"

+ 165 - 0
app/src/main/res/layout/dialog_audio_output.xml

@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Marcel Hibbe <marcel.hibbe@nextcloud.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@color/bg_call_screen_dialog"
+    android:orientation="vertical"
+    android:paddingStart="@dimen/standard_padding"
+    android:paddingEnd="@dimen/standard_padding"
+    android:paddingBottom="@dimen/standard_half_padding">
+
+    <TextView
+        android:id="@+id/upload"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/bottom_sheet_item_height"
+        android:gravity="start|center_vertical"
+        android:text="@string/audio_output_dialog_headline"
+        android:textColor="@color/medium_emphasis_text_dark_background"
+        android:textSize="@dimen/bottom_sheet_text_size" />
+
+    <LinearLayout
+        android:id="@+id/audio_output_bluetooth"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/bottom_sheet_item_height"
+        android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/audio_output_bluetooth_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_baseline_bluetooth_audio_24"
+            app:tint="@color/grey_600" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/audio_output_bluetooth_text"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
+            android:text="@string/audio_output_bluetooth"
+            android:textAlignment="viewStart"
+            android:textColor="@color/high_emphasis_text_dark_background"
+            android:textSize="@dimen/bottom_sheet_text_size" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/audio_output_speaker"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/bottom_sheet_item_height"
+        android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/audio_output_speaker_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_volume_up_white_24dp"
+            app:tint="@color/grey_600" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/audio_output_speaker_text"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
+            android:text="@string/audio_output_speaker"
+            android:textAlignment="viewStart"
+            android:textColor="@color/high_emphasis_text_dark_background"
+            android:textSize="@dimen/bottom_sheet_text_size" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/audio_output_earspeaker"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/bottom_sheet_item_height"
+        android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/audio_output_earspeaker_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_baseline_phone_in_talk_24"
+            app:tint="@color/grey_600" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/audio_output_earspeaker_text"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
+            android:text="@string/audio_output_phone"
+            android:textAlignment="viewStart"
+            android:textColor="@color/high_emphasis_text_dark_background"
+            android:textSize="@dimen/bottom_sheet_text_size" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/audio_output_wired_headset"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/bottom_sheet_item_height"
+        android:background="?android:attr/selectableItemBackground"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/audio_output_wired_headset_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_baseline_headset_mic_24"
+            app:tint="@color/grey_600" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/audio_output_wired_headset_text"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/zero"
+            android:text="@string/audio_output_wired_headset"
+            android:textAlignment="viewStart"
+            android:textColor="@color/high_emphasis_text_dark_background"
+            android:textSize="@dimen/bottom_sheet_text_size" />
+
+    </LinearLayout>
+
+</LinearLayout>

+ 24 - 13
app/src/main/res/layout/menu_item_sheet.xml

@@ -2,6 +2,8 @@
   ~ Nextcloud Talk application
   ~
   ~ @author Mario Danic
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
   ~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
   ~
   ~ This program is free software: you can redistribute it and/or modify
@@ -18,27 +20,36 @@
   ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   -->
 
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     style="@style/MD_ListItem">
-    <RelativeLayout
+
+    <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content">
+        android:layout_height="56dp"
+        android:gravity="center_vertical">
+
         <ImageView
             android:id="@+id/icon"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:scaleType="center"
-            android:layout_centerVertical="true"
-            tools:src="@drawable/ic_delete_grey600_24dp"
             android:layout_marginStart="16dp"
+            android:scaleType="center"
+            app:tint="@color/grey_600"
             tools:ignore="ContentDescription"
-            />
+            tools:src="@drawable/ic_delete_grey600_24dp" />
+
         <com.afollestad.materialdialogs.internal.rtl.RtlTextView
             android:id="@+id/title"
-            tools:text="Item"
-            android:layout_toEndOf="@id/icon"
-            style="@style/MD_ListItemText" />
-    </RelativeLayout>
-</FrameLayout>
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:paddingStart="@dimen/standard_double_padding"
+            android:paddingEnd="@dimen/standard_padding"
+            android:textAlignment="viewStart"
+            android:textColor="@color/high_emphasis_text"
+            android:textSize="@dimen/bottom_sheet_text_size"
+            tools:text="Menu item" />
+    </LinearLayout>
+</FrameLayout>

+ 4 - 3
app/src/main/res/layout/rv_item_menu.xml

@@ -24,7 +24,8 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:background="@color/bg_default">
+    android:background="@color/bg_default"
+    android:minHeight="@dimen/bottom_sheet_item_height">
 
     <TextView
         android:id="@+id/menu_text"
@@ -35,9 +36,9 @@
         android:focusableInTouchMode="false"
         android:gravity="start|center_vertical"
         android:textAlignment="viewStart"
-        android:textColor="@color/conversation_item_header"
+        android:textColor="@color/high_emphasis_text"
         android:textSize="16sp"
-        tools:drawablePadding="16dp"
+        tools:drawablePadding="32dp"
         tools:drawableStart="@drawable/ic_add_grey600_24px"
         tools:text="Start a new conversation" />
 </RelativeLayout>

+ 6 - 0
app/src/main/res/values/colors.xml

@@ -40,6 +40,10 @@
     <color name="medium_emphasis_text">#99000000</color>
     <color name="low_emphasis_text">#61000000</color>
 
+    <!-- general text colors for dark background -->
+    <color name="high_emphasis_text_dark_background">#deffffff</color>
+    <color name="medium_emphasis_text_dark_background">#99ffffff</color>
+
     <!-- Text color of sent messages -->
     <color name="nc_outcoming_text_default">#FFFFFF</color>
     <!-- Text color of received messages -->
@@ -78,6 +82,8 @@
     <color name="bg_message_list_outcoming_bubble_deleted">#800082C9</color>
 
     <color name="bg_bottom_sheet">#46ffffff</color>
+    <color name="bg_call_screen_dialog">#121212</color>
+    <color name="call_screen_text">#ffffffff</color>
 
     <color name="call_buttons_background">#BF999999</color>
     <color name="favorite_icon_tint">#FFCC00</color>

+ 1 - 0
app/src/main/res/values/dimens.xml

@@ -23,6 +23,7 @@
     <dimen name="activity_horizontal_margin">16dp</dimen>
 
     <dimen name="item_height">72dp</dimen>
+    <dimen name="bottom_sheet_item_height">56dp</dimen>
     <dimen name="small_item_height">48dp</dimen>
 
     <dimen name="min_size_clickable_area">48dp</dimen>

+ 5 - 0
app/src/main/res/values/strings.xml

@@ -494,5 +494,10 @@
     <string name="take_photo_send">Send</string>
     <string name="take_photo_error_deleting_picture">Error taking picture</string>
     <string name="take_photo_permission">Taking a photo is not possible without permissions</string>
+    <string name="audio_output_bluetooth">Bluetooth</string>
+    <string name="audio_output_speaker">Speaker</string>
+    <string name="audio_output_phone">Phone</string>
+    <string name="audio_output_dialog_headline">Audio output</string>
+    <string name="audio_output_wired_headset">Wired headset</string>
 
 </resources>

+ 1 - 1
build.gradle

@@ -39,7 +39,7 @@ buildscript {
         classpath 'com.android.tools.build:gradle:4.1.3'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
         classpath 'gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.5'
-        classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.18.1"
+        classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.19.0"
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-554
+552

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 1 error and 222 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 1 error and 223 warnings</span>