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 7bee5a77b..2080ef4af 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/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 availableDevices) { + final MagicAudioManager.AudioDevice currentDevice, + final Set 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 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); } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java index 33f335bd2..c7107010f 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java @@ -50,7 +50,7 @@ public class MenuItem extends AbstractFlexibleItem this.title = title; this.tag = tag; this.icon = icon; - padding = (int) DisplayUtils.convertDpToPixel(16, + padding = (int) DisplayUtils.convertDpToPixel(32, NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext()); } 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 new file mode 100644 index 000000000..be3976b9c --- /dev/null +++ b/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 + * + * 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 . + */ + +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(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + companion object { + private const val TAG = "AudioOutputDialog" + } +} 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..866e5c6ce 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java +++ b/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 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 + ")"); + private void setAudioDeviceInternal(AudioDevice audioDevice) { + Log.d(TAG, "setAudioDeviceInternal(device=" + audioDevice + ")"); - if (audioDevices.contains(device)) { - - 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,35 +301,10 @@ public class MagicAudioManager { Log.e(TAG, "Invalid audio device selection"); break; } - selectedAudioDevice = device; + currentAudioDevice = audioDevice; } } - /** - * 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. */ @@ -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 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 (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { - // If user selected speaker phone, but then plugged wired headset then make - // wired headset as user selected device. - 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.BLUETOOTH + && bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE) { userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; } + if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE && hasWiredHeadset) { + userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; + } + 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 availableAudioDevices); + AudioDevice selectedAudioDevice, Set availableAudioDevices); } /* Receiver which handles changes in wired headset availability. */ diff --git a/app/src/main/res/drawable/ic_baseline_attachment_24.xml b/app/src/main/res/drawable/ic_baseline_attachment_24.xml deleted file mode 100644 index 91664a98e..000000000 --- a/app/src/main/res/drawable/ic_baseline_attachment_24.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_baseline_bluetooth_audio_24.xml b/app/src/main/res/drawable/ic_baseline_bluetooth_audio_24.xml new file mode 100644 index 000000000..3ac2dc4d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_bluetooth_audio_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_headset_mic_24.xml b/app/src/main/res/drawable/ic_baseline_headset_mic_24.xml new file mode 100644 index 000000000..31cc591d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_headset_mic_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_phone_in_talk_24.xml b/app/src/main/res/drawable/ic_baseline_phone_in_talk_24.xml new file mode 100644 index 000000000..42e266da8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_phone_in_talk_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml index ca1c5ddbe..897c6ed06 100644 --- a/app/src/main/res/layout/call_activity.xml +++ b/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"> + app:roundAsCircle="true" + android:layout_weight="1"/> + app:roundAsCircle="true" + android:layout_weight="1"/> + app:roundAsCircle="true" + android:layout_weight="1"/> + app:roundAsCircle="true" + android:layout_weight="1"/> + app:roundAsCircle="true" + android:layout_weight="1"/> + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + android:paddingBottom="@dimen/standard_half_padding"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/menu_item_sheet.xml b/app/src/main/res/layout/menu_item_sheet.xml index caa3612d5..04d629a1a 100644 --- a/app/src/main/res/layout/menu_item_sheet.xml +++ b/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 ~ Copyright (C) 2017-2019 Mario Danic ~ ~ This program is free software: you can redistribute it and/or modify @@ -18,27 +20,36 @@ ~ along with this program. If not, see . --> - - + android:layout_height="56dp" + android:gravity="center_vertical"> + + tools:src="@drawable/ic_delete_grey600_24dp" /> + - - \ No newline at end of file + 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" /> + + diff --git a/app/src/main/res/layout/rv_item_menu.xml b/app/src/main/res/layout/rv_item_menu.xml index 1807c7580..7e9ec7fd0 100644 --- a/app/src/main/res/layout/rv_item_menu.xml +++ b/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"> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 61726802b..8b059a897 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -40,6 +40,10 @@ #99000000 #61000000 + + #deffffff + #99ffffff + #FFFFFF @@ -78,6 +82,8 @@ #800082C9 #46ffffff + #121212 + #ffffffff #BF999999 #FFCC00 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e52c554ed..59b913073 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -23,6 +23,7 @@ 16dp 72dp + 56dp 48dp 48dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94ca857b3..2bef3843a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -494,5 +494,10 @@ Send Error taking picture Taking a photo is not possible without permissions + Bluetooth + Speaker + Phone + Audio output + Wired headset diff --git a/build.gradle b/build.gradle index 50d2871e9..66b057b22 100644 --- a/build.gradle +++ b/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 diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 8d28c8c9e..4a722e9c7 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -554 \ No newline at end of file +552 \ No newline at end of file diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index 96101281e..de06c6be4 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 1 error and 222 warnings + Lint Report: 1 error and 223 warnings