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

Feature/1761/select audio output
This commit is contained in:
Marcel Hibbe 2022-02-08 12:46:20 +01:00 committed by GitHub
commit f076a81427
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 601 additions and 295 deletions

View File

@ -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);
}

View File

@ -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());
}

View File

@ -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"
}
}

View File

@ -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 + ")");
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<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 (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<AudioDevice> availableAudioDevices);
AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
}
/* Receiver which handles changes in wired headset availability. */

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -1 +1 @@
554
552

View File

@ -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>