Refactor call audio routing and bluetooth management.

fork-5.53.8
Cody Henthorne 2021-09-27 11:23:10 -04:00
rodzic 6c55916cda
commit e637f15a43
36 zmienionych plików z 1345 dodań i 981 usunięć

Wyświetl plik

@ -17,8 +17,6 @@
package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;
@ -79,6 +77,7 @@ import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
@ -86,6 +85,8 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
private static final String TAG = Log.tag(WebRtcCallActivity.class);
@ -366,15 +367,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void handleSetAudioHandset() {
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(false);
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.EARPIECE);
}
private void handleSetAudioSpeaker() {
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(true);
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE);
}
private void handleSetAudioBluetooth() {
ApplicationDependencies.getSignalCallManager().setAudioBluetooth(true);
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.BLUETOOTH);
}
private void handleSetMuteAudio(boolean enabled) {

Wyświetl plik

@ -221,7 +221,7 @@ data class CallParticipantsState(
localRenderState = localRenderState,
showVideoForOutgoing = newShowVideoForOutgoing,
remoteDevicesCount = webRtcViewModel.remoteDevicesCount,
ringGroup = webRtcViewModel.shouldRingGroup(),
ringGroup = webRtcViewModel.ringGroup,
isInOutgoingRingingMode = isInOutgoingRingingMode,
ringerRecipient = webRtcViewModel.ringerRecipient
)

Wyświetl plik

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
@ -10,11 +9,9 @@ import androidx.core.util.Consumer;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.util.Collections;
import java.util.List;
@ -22,21 +19,9 @@ import java.util.List;
class WebRtcCallRepository {
private final Context context;
private final AudioManager audioManager;
WebRtcCallRepository(@NonNull Context context) {
this.context = context;
this.audioManager = ServiceUtil.getAudioManager(ApplicationDependencies.getApplication());
}
@NonNull WebRtcAudioOutput getAudioOutput() {
if (audioManager.isBluetoothScoOn()) {
return WebRtcAudioOutput.HEADSET;
} else if (audioManager.isSpeakerphoneOn()) {
return WebRtcAudioOutput.SPEAKER;
} else {
return WebRtcAudioOutput.HANDSET;
}
}
@WorkerThread

Wyświetl plik

@ -19,7 +19,6 @@ import com.annimon.stream.Stream;
import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
@ -35,11 +34,13 @@ import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public class WebRtcCallViewModel extends ViewModel {
@ -252,9 +253,9 @@ public class WebRtcCallViewModel extends ViewModel {
webRtcViewModel.isRemoteVideoEnabled(),
webRtcViewModel.isRemoteVideoOffer(),
localParticipant.isMoreThanOneCameraAvailable(),
webRtcViewModel.isBluetoothAvailable(),
Util.hasItems(webRtcViewModel.getRemoteParticipants()),
repository.getAudioOutput(),
webRtcViewModel.getActiveDevice(),
webRtcViewModel.getAvailableDevices(),
webRtcViewModel.getRemoteDevicesCount().orElse(0),
webRtcViewModel.getParticipantLimit());
@ -314,9 +315,9 @@ public class WebRtcCallViewModel extends ViewModel {
boolean isRemoteVideoEnabled,
boolean isRemoteVideoOffer,
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean hasAtLeastOneRemote,
@NonNull WebRtcAudioOutput audioOutput,
@NonNull SignalAudioManager.AudioDevice activeDevice,
@NonNull Set<SignalAudioManager.AudioDevice> availableDevices,
long remoteDevicesCount,
@Nullable Long participantLimit)
{
@ -373,14 +374,14 @@ public class WebRtcCallViewModel extends ViewModel {
webRtcControls.setValue(new WebRtcControls(isLocalVideoEnabled,
isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
Boolean.TRUE.equals(isInPipMode.getValue()),
hasAtLeastOneRemote,
callState,
groupCallState,
audioOutput,
participantLimit,
WebRtcControls.FoldableState.flat()));
WebRtcControls.FoldableState.flat(),
activeDevice,
availableDevices));
}
private @NonNull WebRtcControls updateControlsFoldableState(@NonNull WebRtcControls.FoldableState foldableState, @NonNull WebRtcControls controls) {

Wyświetl plik

@ -9,65 +9,90 @@ import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import java.util.Set;
import static java.util.Collections.emptySet;
public final class WebRtcControls {
public static final WebRtcControls NONE = new WebRtcControls();
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null, FoldableState.flat());
public static final WebRtcControls PIP = new WebRtcControls(false,
false,
false,
true,
false,
CallState.NONE,
GroupCallState.NONE,
null,
FoldableState.flat(),
SignalAudioManager.AudioDevice.NONE,
emptySet());
private final boolean isRemoteVideoEnabled;
private final boolean isLocalVideoEnabled;
private final boolean isMoreThanOneCameraAvailable;
private final boolean isBluetoothAvailable;
private final boolean isInPipMode;
private final boolean hasAtLeastOneRemote;
private final CallState callState;
private final GroupCallState groupCallState;
private final WebRtcAudioOutput audioOutput;
private final Long participantLimit;
private final FoldableState foldableState;
private final boolean isRemoteVideoEnabled;
private final boolean isLocalVideoEnabled;
private final boolean isMoreThanOneCameraAvailable;
private final boolean isInPipMode;
private final boolean hasAtLeastOneRemote;
private final CallState callState;
private final GroupCallState groupCallState;
private final Long participantLimit;
private final FoldableState foldableState;
private final SignalAudioManager.AudioDevice activeDevice;
private final Set<SignalAudioManager.AudioDevice> availableDevices;
private WebRtcControls() {
this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null, FoldableState.flat());
this(false,
false,
false,
false,
false,
CallState.NONE,
GroupCallState.NONE,
null,
FoldableState.flat(),
SignalAudioManager.AudioDevice.NONE,
emptySet());
}
WebRtcControls(boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled,
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean isInPipMode,
boolean hasAtLeastOneRemote,
@NonNull CallState callState,
@NonNull GroupCallState groupCallState,
@NonNull WebRtcAudioOutput audioOutput,
@Nullable Long participantLimit,
@NonNull FoldableState foldableState)
@NonNull FoldableState foldableState,
@NonNull SignalAudioManager.AudioDevice activeDevice,
@NonNull Set<SignalAudioManager.AudioDevice> availableDevices)
{
this.isLocalVideoEnabled = isLocalVideoEnabled;
this.isRemoteVideoEnabled = isRemoteVideoEnabled;
this.isBluetoothAvailable = isBluetoothAvailable;
this.isMoreThanOneCameraAvailable = isMoreThanOneCameraAvailable;
this.isInPipMode = isInPipMode;
this.hasAtLeastOneRemote = hasAtLeastOneRemote;
this.callState = callState;
this.groupCallState = groupCallState;
this.audioOutput = audioOutput;
this.participantLimit = participantLimit;
this.foldableState = foldableState;
this.activeDevice = activeDevice;
this.availableDevices = availableDevices;
}
public @NonNull WebRtcControls withFoldableState(FoldableState foldableState) {
return new WebRtcControls(isLocalVideoEnabled,
isRemoteVideoEnabled,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
isInPipMode,
hasAtLeastOneRemote,
callState,
groupCallState,
audioOutput,
participantLimit,
foldableState);
foldableState,
activeDevice,
availableDevices);
}
boolean displayErrorControls() {
@ -129,7 +154,7 @@ public final class WebRtcControls {
}
boolean displayAudioToggle() {
return (isPreJoin() || isAtLeastOutgoing()) && (!isLocalVideoEnabled || isBluetoothAvailable);
return (isPreJoin() || isAtLeastOutgoing()) && (!isLocalVideoEnabled || enableHeadsetInAudioToggle());
}
boolean displayCameraToggle() {
@ -153,7 +178,7 @@ public final class WebRtcControls {
}
boolean enableHeadsetInAudioToggle() {
return isBluetoothAvailable;
return availableDevices.contains(SignalAudioManager.AudioDevice.BLUETOOTH);
}
boolean isFadeOutEnabled() {
@ -173,7 +198,14 @@ public final class WebRtcControls {
}
@NonNull WebRtcAudioOutput getAudioOutput() {
return audioOutput;
switch (activeDevice) {
case SPEAKER_PHONE:
return WebRtcAudioOutput.SPEAKER;
case BLUETOOTH:
return WebRtcAudioOutput.HEADSET;
default:
return WebRtcAudioOutput.HANDSET;
}
}
boolean showSmallHeader() {

Wyświetl plik

@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IasKeyStore;
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
@ -100,8 +101,9 @@ public class ApplicationDependencies {
private static volatile TextSecureSessionStore sessionStore;
private static volatile TextSecurePreKeyStore preKeyStore;
private static volatile SignalSenderKeyStore senderKeyStore;
private static volatile GiphyMp4Cache giphyMp4Cache;
private static volatile SimpleExoPlayerPool exoPlayerPool;
private static volatile GiphyMp4Cache giphyMp4Cache;
private static volatile SimpleExoPlayerPool exoPlayerPool;
private static volatile AudioManagerCompat audioManagerCompat;
@MainThread
public static void init(@NonNull Application application, @NonNull Provider provider) {
@ -577,6 +579,17 @@ public class ApplicationDependencies {
return exoPlayerPool;
}
public static @NonNull AudioManagerCompat getAndroidCallAudioManager() {
if (audioManagerCompat == null) {
synchronized (LOCK) {
if (audioManagerCompat == null) {
audioManagerCompat = provider.provideAndroidCallAudioManager();
}
}
}
return audioManagerCompat;
}
public interface Provider {
@NonNull GroupsV2Operations provideGroupsV2Operations();
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
@ -611,5 +624,6 @@ public class ApplicationDependencies {
@NonNull SignalSenderKeyStore provideSenderKeyStore();
@NonNull GiphyMp4Cache provideGiphyMp4Cache();
@NonNull SimpleExoPlayerPool provideExoPlayerPool();
@NonNull AudioManagerCompat provideAndroidCallAudioManager();
}
}

Wyświetl plik

@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.util.FrameRateTracker;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
@ -295,6 +296,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
return new SimpleExoPlayerPool(context);
}
@Override
public @NonNull AudioManagerCompat provideAndroidCallAudioManager() {
return AudioManagerCompat.create(context);
}
private @NonNull WebSocketFactory provideWebSocketFactory(@NonNull SignalWebSocketHealthMonitor healthMonitor) {
return new WebSocketFactory() {
@Override

Wyświetl plik

@ -1,214 +0,0 @@
package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.OptionalLong;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import java.util.List;
import java.util.Set;
public class WebRtcViewModel {
public enum State {
IDLE,
// Normal states
CALL_PRE_JOIN,
CALL_INCOMING,
CALL_OUTGOING,
CALL_CONNECTED,
CALL_RINGING,
CALL_BUSY,
CALL_DISCONNECTED,
CALL_NEEDS_PERMISSION,
// Error states
NETWORK_FAILURE,
RECIPIENT_UNAVAILABLE,
NO_SUCH_USER,
UNTRUSTED_IDENTITY,
// Multiring Hangup States
CALL_ACCEPTED_ELSEWHERE,
CALL_DECLINED_ELSEWHERE,
CALL_ONGOING_ELSEWHERE;
public boolean isErrorState() {
return this == NETWORK_FAILURE ||
this == RECIPIENT_UNAVAILABLE ||
this == NO_SUCH_USER ||
this == UNTRUSTED_IDENTITY;
}
public boolean isPreJoinOrNetworkUnavailable() {
return this == CALL_PRE_JOIN || this == NETWORK_FAILURE;
}
public boolean isPassedPreJoin() {
return this.ordinal() > CALL_PRE_JOIN.ordinal();
}
}
public enum GroupCallState {
IDLE,
RINGING,
DISCONNECTED,
CONNECTING,
RECONNECTING,
CONNECTED,
CONNECTED_AND_JOINING,
CONNECTED_AND_JOINED;
public boolean isIdle() {
return this == IDLE;
}
public boolean isNotIdle() {
return this != IDLE;
}
public boolean isConnected() {
switch (this) {
case CONNECTED:
case CONNECTED_AND_JOINING:
case CONNECTED_AND_JOINED:
return true;
}
return false;
}
public boolean isNotIdleOrConnected() {
switch (this) {
case DISCONNECTED:
case CONNECTING:
case RECONNECTING:
return true;
}
return false;
}
public boolean isRinging() {
return this == RINGING;
}
}
private final @NonNull State state;
private final @NonNull GroupCallState groupState;
private final @NonNull Recipient recipient;
private final boolean isBluetoothAvailable;
private final boolean isRemoteVideoOffer;
private final long callConnectedTime;
private final CallParticipant localParticipant;
private final List<CallParticipant> remoteParticipants;
private final Set<RecipientId> identityChangedRecipients;
private final OptionalLong remoteDevicesCount;
private final Long participantLimit;
private final boolean ringGroup;
private final Recipient ringerRecipient;
public WebRtcViewModel(@NonNull WebRtcServiceState state) {
this.state = state.getCallInfoState().getCallState();
this.groupState = state.getCallInfoState().getGroupCallState();
this.recipient = state.getCallInfoState().getCallRecipient();
this.isRemoteVideoOffer = state.getCallSetupState().isRemoteVideoOffer();
this.isBluetoothAvailable = state.getLocalDeviceState().isBluetoothAvailable();
this.remoteParticipants = state.getCallInfoState().getRemoteCallParticipants();
this.identityChangedRecipients = state.getCallInfoState().getIdentityChangedRecipients();
this.callConnectedTime = state.getCallInfoState().getCallConnectedTime();
this.remoteDevicesCount = state.getCallInfoState().getRemoteDevicesCount();
this.participantLimit = state.getCallInfoState().getParticipantLimit();
this.ringGroup = state.getCallSetupState().shouldRingGroup();
this.ringerRecipient = state.getCallSetupState().getRingerRecipient();
this.localParticipant = CallParticipant.createLocal(state.getLocalDeviceState().getCameraState(),
state.getVideoState().getLocalSink() != null ? state.getVideoState().getLocalSink()
: new BroadcastVideoSink(),
state.getLocalDeviceState().isMicrophoneEnabled());
}
public @NonNull State getState() {
return state;
}
public @NonNull GroupCallState getGroupState() {
return groupState;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
public boolean isRemoteVideoEnabled() {
return Stream.of(remoteParticipants).anyMatch(CallParticipant::isVideoEnabled) || (groupState.isNotIdle() && remoteParticipants.size() > 1);
}
public boolean isBluetoothAvailable() {
return isBluetoothAvailable;
}
public boolean isRemoteVideoOffer() {
return isRemoteVideoOffer;
}
public long getCallConnectedTime() {
return callConnectedTime;
}
public @NonNull CallParticipant getLocalParticipant() {
return localParticipant;
}
public @NonNull List<CallParticipant> getRemoteParticipants() {
return remoteParticipants;
}
public @NonNull Set<RecipientId> getIdentityChangedParticipants() {
return identityChangedRecipients;
}
public OptionalLong getRemoteDevicesCount() {
return remoteDevicesCount;
}
public boolean areRemoteDevicesInCall() {
return remoteDevicesCount.isPresent() && remoteDevicesCount.getAsLong() > 0;
}
public @Nullable Long getParticipantLimit() {
return participantLimit;
}
public boolean shouldRingGroup() {
return ringGroup;
}
public @NonNull Recipient getRingerRecipient() {
return ringerRecipient;
}
@Override
public @NonNull String toString() {
return "WebRtcViewModel{" +
"state=" + state +
", recipient=" + recipient.getId() +
", isBluetoothAvailable=" + isBluetoothAvailable +
", isRemoteVideoOffer=" + isRemoteVideoOffer +
", callConnectedTime=" + callConnectedTime +
", localParticipant=" + localParticipant +
", remoteParticipants=" + remoteParticipants +
", identityChangedRecipients=" + identityChangedRecipients +
", remoteDevicesCount=" + remoteDevicesCount +
", participantLimit=" + participantLimit +
'}';
}
}

Wyświetl plik

@ -0,0 +1,128 @@
package org.thoughtcrime.securesms.events
import com.annimon.stream.OptionalLong
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink
import org.thoughtcrime.securesms.events.CallParticipant.Companion.createLocal
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
class WebRtcViewModel(state: WebRtcServiceState) {
enum class State {
IDLE,
// Normal states
CALL_PRE_JOIN,
CALL_INCOMING,
CALL_OUTGOING,
CALL_CONNECTED,
CALL_RINGING,
CALL_BUSY,
CALL_DISCONNECTED,
CALL_NEEDS_PERMISSION,
// Error states
NETWORK_FAILURE,
RECIPIENT_UNAVAILABLE,
NO_SUCH_USER,
UNTRUSTED_IDENTITY,
// Multiring Hangup States
CALL_ACCEPTED_ELSEWHERE,
CALL_DECLINED_ELSEWHERE,
CALL_ONGOING_ELSEWHERE;
val isErrorState: Boolean
get() = this == NETWORK_FAILURE || this == RECIPIENT_UNAVAILABLE || this == NO_SUCH_USER || this == UNTRUSTED_IDENTITY
val isPreJoinOrNetworkUnavailable: Boolean
get() = this == CALL_PRE_JOIN || this == NETWORK_FAILURE
val isPassedPreJoin: Boolean
get() = ordinal > ordinal
}
enum class GroupCallState {
IDLE,
RINGING,
DISCONNECTED,
CONNECTING,
RECONNECTING,
CONNECTED,
CONNECTED_AND_JOINING,
CONNECTED_AND_JOINED;
val isIdle: Boolean
get() = this == IDLE
val isNotIdle: Boolean
get() = this != IDLE
val isConnected: Boolean
get() {
return when (this) {
CONNECTED, CONNECTED_AND_JOINING, CONNECTED_AND_JOINED -> true
else -> false
}
}
val isNotIdleOrConnected: Boolean
get() {
return when (this) {
DISCONNECTED, CONNECTING, RECONNECTING -> true
else -> false
}
}
val isRinging: Boolean
get() = this == RINGING
}
val state: State = state.callInfoState.callState
val groupState: GroupCallState = state.callInfoState.groupCallState
val recipient: Recipient = state.callInfoState.callRecipient
val isRemoteVideoOffer: Boolean = state.callSetupState.isRemoteVideoOffer
val callConnectedTime: Long = state.callInfoState.callConnectedTime
val remoteParticipants: List<CallParticipant> = state.callInfoState.remoteCallParticipants
val identityChangedParticipants: Set<RecipientId> = state.callInfoState.identityChangedRecipients
val remoteDevicesCount: OptionalLong = state.callInfoState.remoteDevicesCount
val participantLimit: Long? = state.callInfoState.participantLimit
@get:JvmName("shouldRingGroup")
val ringGroup: Boolean = state.callSetupState.ringGroup
val ringerRecipient: Recipient = state.callSetupState.ringerRecipient
val activeDevice: SignalAudioManager.AudioDevice = state.localDeviceState.activeDevice
val availableDevices: Set<SignalAudioManager.AudioDevice> = state.localDeviceState.availableDevices
val localParticipant: CallParticipant = createLocal(
state.localDeviceState.cameraState,
(if (state.videoState.localSink != null) state.videoState.localSink else BroadcastVideoSink())!!,
state.localDeviceState.isMicrophoneEnabled
)
val isRemoteVideoEnabled: Boolean
get() = remoteParticipants.any(CallParticipant::isVideoEnabled) || groupState.isNotIdle && remoteParticipants.size > 1
fun areRemoteDevicesInCall(): Boolean {
return remoteDevicesCount.isPresent && remoteDevicesCount.asLong > 0
}
override fun toString(): String {
return """
WebRtcViewModel {
state=$state,
recipient=${recipient.id},
isRemoteVideoOffer=$isRemoteVideoOffer,
callConnectedTime=$callConnectedTime,
localParticipant=$localParticipant,
remoteParticipants=$remoteParticipants,
identityChangedRecipients=$identityChangedParticipants,
remoteDevicesCount=$remoteDevicesCount,
participantLimit=$participantLimit,
activeDevice=$activeDevice,
availableDevices=$availableDevices,
}
""".trimIndent()
}
}

Wyświetl plik

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
@ -13,7 +11,6 @@ import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_CONNECTING;
@ -76,9 +73,6 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
Log.i(tag, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, remotePeer);
webRtcInteractor.retrieveTurnServers(remotePeer);

Wyświetl plik

@ -7,11 +7,11 @@ import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.CallState;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
@ -39,7 +39,7 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
ApplicationDependencies.getAppForegroundObserver().removeListener(webRtcInteractor.getForegroundListener());
webRtcInteractor.startAudioCommunication(activePeer.getState() == CallState.REMOTE_RINGING);
webRtcInteractor.startAudioCommunication();
activePeer.connected();
@ -56,12 +56,10 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
.callConnectedTime(System.currentTimeMillis())
.commit()
.changeLocalDeviceState()
.wantsBluetooth(true)
.build();
webRtcInteractor.setCallInProgressNotification(TYPE_ESTABLISHED, activePeer);
webRtcInteractor.unregisterPowerButtonReceiver();
webRtcInteractor.setWantsBluetoothConnection(true);
try {
CallManager callManager = webRtcInteractor.getCallManager();
@ -77,6 +75,12 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
currentState = currentState.getActionProcessor().handleSetEnableVideo(currentState, true);
}
if (currentState.getCallSetupState().isAcceptWithVideo() || currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.setDefaultAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE, false);
} else {
webRtcInteractor.setDefaultAudioDevice(SignalAudioManager.AudioDevice.EARPIECE, false);
}
return currentState;
}
@ -98,7 +102,7 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
.cameraState(camera.getCameraState())
.build();
WebRtcUtil.enableSpeakerPhoneIfNeeded(context, currentState.getCallSetupState().isEnableVideoOnCreate());
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor, currentState);
return currentState;
}

Wyświetl plik

@ -55,7 +55,7 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
WebRtcUtil.enableSpeakerPhoneIfNeeded(context, currentState.getLocalDeviceState().getCameraState().isEnabled());
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor, currentState);
return currentState;
}

Wyświetl plik

@ -1,15 +1,14 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import java.util.Set;
/**
* Encapsulates the shared logic to deal with local device actions. Other action processors inherit
@ -23,76 +22,29 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleWiredHeadsetChange(@NonNull WebRtcServiceState currentState, boolean present) {
Log.i(tag, "handleWiredHeadsetChange():");
protected @NonNull WebRtcServiceState handleAudioDeviceChanged(@NonNull WebRtcServiceState currentState, @NonNull SignalAudioManager.AudioDevice activeDevice, @NonNull Set<SignalAudioManager.AudioDevice> availableDevices) {
Log.i(tag, "handleAudioDeviceChanged(): active: " + activeDevice + " available: " + availableDevices);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
if (present && androidAudioManager.isSpeakerphoneOn()) {
androidAudioManager.setSpeakerphoneOn(false);
androidAudioManager.setBluetoothScoOn(false);
} else if (!present && !androidAudioManager.isSpeakerphoneOn() && !androidAudioManager.isBluetoothScoOn() && currentState.getLocalDeviceState().getCameraState().isEnabled()) {
androidAudioManager.setSpeakerphoneOn(true);
if (!currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
webRtcInteractor.postStateUpdate(currentState);
return currentState.builder()
.changeLocalDeviceState()
.setActiveDevice(activeDevice)
.setAvailableDevices(availableDevices)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetUserAudioDevice(@NonNull WebRtcServiceState currentState, @NonNull SignalAudioManager.AudioDevice userDevice) {
Log.i(tag, "handleSetUserAudioDevice(): userDevice: " + userDevice);
webRtcInteractor.setUserAudioDevice(userDevice);
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleBluetoothChange(@NonNull WebRtcServiceState currentState, boolean available) {
Log.i(tag, "handleBluetoothChange(): " + available);
if (available && currentState.getLocalDeviceState().wantsBluetooth()) {
webRtcInteractor.setWantsBluetoothConnection(true);
}
return currentState.builder()
.changeLocalDeviceState()
.isBluetoothAvailable(available)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetSpeakerAudio(@NonNull WebRtcServiceState currentState, boolean isSpeaker) {
Log.i(tag, "handleSetSpeakerAudio(): " + isSpeaker);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
webRtcInteractor.setWantsBluetoothConnection(false);
androidAudioManager.setSpeakerphoneOn(isSpeaker);
if (!currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
webRtcInteractor.postStateUpdate(currentState);
return currentState.builder()
.changeLocalDeviceState()
.wantsBluetooth(false)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetBluetoothAudio(@NonNull WebRtcServiceState currentState, boolean isBluetooth) {
Log.i(tag, "handleSetBluetoothAudio(): " + isBluetooth);
webRtcInteractor.setWantsBluetoothConnection(isBluetooth);
if (!currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
webRtcInteractor.postStateUpdate(currentState);
return currentState.builder()
.changeLocalDeviceState()
.wantsBluetooth(isBluetooth)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetCameraFlip(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleSetCameraFlip():");

Wyświetl plik

@ -84,7 +84,7 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
.cameraState(camera.getCameraState())
.build();
WebRtcUtil.enableSpeakerPhoneIfNeeded(context, currentState.getCallSetupState().isEnableVideoOnCreate());
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor, currentState);
return currentState;
}

Wyświetl plik

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
@ -18,6 +16,8 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
/**
* Process actions to go from lobby to a joined call.
*/
@ -62,7 +62,8 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
case CONNECTED:
if (device.getJoinState() == GroupCall.JoinState.JOINED) {
webRtcInteractor.startAudioCommunication(true);
webRtcInteractor.setCallInProgressNotification(TYPE_ESTABLISHED, currentState.getCallInfoState().getCallRecipient());
webRtcInteractor.startAudioCommunication();
if (currentState.getLocalDeviceState().getCameraState().isEnabled()) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IN_VIDEO);
@ -70,9 +71,6 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
}
webRtcInteractor.setCallInProgressNotification(TYPE_ESTABLISHED, currentState.getCallInfoState().getCallRecipient());
webRtcInteractor.setWantsBluetoothConnection(true);
try {
groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled());
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
@ -96,7 +94,6 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
.callConnectedTime(System.currentTimeMillis())
.commit()
.changeLocalDeviceState()
.wantsBluetooth(true)
.commit()
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor));
} else if (device.getJoinState() == GroupCall.JoinState.JOINING) {
@ -152,7 +149,7 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
.cameraState(camera.getCameraState())
.build();
WebRtcUtil.enableSpeakerPhoneIfNeeded(context, currentState.getCallSetupState().isEnableVideoOnCreate());
WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor, currentState);
return currentState;
}

Wyświetl plik

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
@ -20,7 +18,6 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.List;
@ -148,13 +145,9 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
currentState = WebRtcVideoUtil.reinitializeCamera(context, webRtcInteractor.getCameraEventListener(), currentState);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, currentState.getCallInfoState().getCallRecipient());
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, currentState.getCallInfoState().getCallRecipient());
webRtcInteractor.setWantsBluetoothConnection(true);
try {
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
@ -174,7 +167,6 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
.commit()
.changeLocalDeviceState()
.wantsBluetooth(true)
.build();
}

Wyświetl plik

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import android.net.Uri;
import androidx.annotation.NonNull;
@ -23,7 +22,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.libsignal.util.guava.Optional;
@ -108,11 +106,9 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, remotePeerGroup);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
webRtcInteractor.initializeAudioForCall();
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext());
if (shouldDisturbUserWithCall) {
@ -123,7 +119,6 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
}
}
webRtcInteractor.initializeAudioForCall();
if (shouldDisturbUserWithCall && SignalStore.settings().isCallNotificationsEnabled()) {
Uri ringtone = recipient.resolve().getCallRingtone();
RecipientDatabase.VibrateState vibrateState = recipient.resolve().getCallVibrate();
@ -135,7 +130,6 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
webRtcInteractor.startIncomingRinger(ringtone, vibrateState == RecipientDatabase.VibrateState.ENABLED || (vibrateState == RecipientDatabase.VibrateState.DEFAULT && SignalStore.settings().isCallVibrateEnabled()));
}
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, remotePeerGroup);
webRtcInteractor.registerPowerButtonReceiver();
return currentState.builder()
@ -196,13 +190,9 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
.enableVideoOnCreate(answerWithVideo)
.build();
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, currentState.getCallInfoState().getCallRecipient());
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, currentState.getCallInfoState().getCallRecipient());
webRtcInteractor.setWantsBluetoothConnection(true);
try {
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
@ -222,7 +212,6 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
.commit()
.changeLocalDeviceState()
.wantsBluetooth(true)
.build();
}
@ -246,7 +235,6 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
webRtcInteractor.stopAudio(false);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService();

Wyświetl plik

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
@ -23,7 +22,7 @@ import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
@ -66,17 +65,14 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
Log.i(TAG, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
WebRtcUtil.enableSpeakerPhoneIfNeeded(context, currentState.getCallSetupState().isEnableVideoOnCreate());
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer);
webRtcInteractor.setDefaultAudioDevice(currentState.getCallSetupState().isEnableVideoOnCreate() ? SignalAudioManager.AudioDevice.SPEAKER_PHONE
: SignalAudioManager.AudioDevice.EARPIECE,
false);
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
webRtcInteractor.startOutgoingRinger();
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer);
webRtcInteractor.setWantsBluetoothConnection(true);
RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, Recipient.resolved(remotePeer.getId()), DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(remotePeer.getId()));
DatabaseFactory.getSmsDatabase(context).insertOutgoingCall(remotePeer.getId(), currentState.getCallSetupState().isEnableVideoOnCreate());
@ -87,7 +83,6 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
.callState(WebRtcViewModel.State.CALL_OUTGOING)
.commit()
.changeLocalDeviceState()
.wantsBluetooth(true)
.build();
}

Wyświetl plik

@ -1,12 +1,5 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.GroupCallState.IDLE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.CALL_INCOMING;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NETWORK_FAILURE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NO_SUCH_USER;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.UNTRUSTED_IDENTITY;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcUtil.getUrgencyFromCallUrgency;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
@ -85,6 +78,13 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.GroupCallState.IDLE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.CALL_INCOMING;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NETWORK_FAILURE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NO_SUCH_USER;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.UNTRUSTED_IDENTITY;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcUtil.getUrgencyFromCallUrgency;
/**
* Entry point for all things calling. Lives for the life of the app instance and will spin up a foreground service when needed to
* handle "active" calls.
@ -126,7 +126,6 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
this.serviceState = new WebRtcServiceState(new IdleActionProcessor(new WebRtcInteractor(this.context,
this,
lockManager,
new SignalAudioManager(context),
this,
this,
this)));
@ -193,14 +192,6 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleOrientationChanged(s, isLandscapeEnabled, degrees));
}
public void setAudioSpeaker(boolean isSpeaker) {
process((s, p) -> p.handleSetSpeakerAudio(s, isSpeaker));
}
public void setAudioBluetooth(boolean isBluetooth) {
process((s, p) -> p.handleSetBluetoothAudio(s, isBluetooth));
}
public void setMuteAudio(boolean enabled) {
process((s, p) -> p.handleSetMuteAudio(s, enabled));
}
@ -237,10 +228,6 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleIsInCallQuery(s, resultReceiver));
}
public void wiredHeadsetChange(boolean available) {
process((s, p) -> p.handleWiredHeadsetChange(s, available));
}
public void networkChange(boolean available) {
process((s, p) -> p.handleNetworkChanged(s, available));
}
@ -253,10 +240,6 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleScreenOffChange(s));
}
public void bluetoothChange(boolean available) {
process((s, p) -> p.handleBluetoothChange(s, available));
}
public void postStateUpdate(@NonNull WebRtcServiceState state) {
EventBus.getDefault().postSticky(new WebRtcViewModel(state));
}
@ -299,6 +282,14 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleReceivedGroupCallPeekForRingingCheck(s, groupCallRingCheckInfo, deviceCount));
}
public void onAudioDeviceChanged(@NonNull SignalAudioManager.AudioDevice activeDevice, @NonNull Set<SignalAudioManager.AudioDevice> availableDevices) {
process((s, p) -> p.handleAudioDeviceChanged(s, activeDevice, availableDevices));
}
public void selectAudioDevice(@NonNull SignalAudioManager.AudioDevice desiredDevice) {
process((s, p) -> p.handleSetUserAudioDevice(s, desiredDevice));
}
public void peekGroupCall(@NonNull RecipientId id) {
if (callManager == null) {
Log.i(TAG, "Unable to peekGroupCall, call manager is null");

Wyświetl plik

@ -1,9 +1,5 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.AnswerMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.HangupMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedAnswerMetadata;
import android.content.Context;
import android.os.ResultReceiver;
@ -39,6 +35,7 @@ import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.IdentityKey;
@ -52,8 +49,13 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.AnswerMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.HangupMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedAnswerMetadata;
/**
* Base WebRTC action processor and core of the calling state machine. As actions (as intents)
* are sent to the service, they are passed to an instance of the current state's action processor.
@ -370,6 +372,16 @@ public abstract class WebRtcActionProcessor {
return builder.build();
}
protected @NonNull WebRtcServiceState handleAudioDeviceChanged(@NonNull WebRtcServiceState currentState, @NonNull SignalAudioManager.AudioDevice activeDevice, @NonNull Set<SignalAudioManager.AudioDevice> availableDevices) {
Log.i(tag, "handleAudioDeviceChanged not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSetUserAudioDevice(@NonNull WebRtcServiceState currentState, @NonNull SignalAudioManager.AudioDevice userDevice) {
Log.i(tag, "handleSetUserAudioDevice not processed");
return currentState;
}
//endregion Active call
//region Call setup
@ -410,16 +422,6 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleSetSpeakerAudio(@NonNull WebRtcServiceState currentState, boolean isSpeaker) {
Log.i(tag, "handleSetSpeakerAudio not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSetBluetoothAudio(@NonNull WebRtcServiceState currentState, boolean isBluetooth) {
Log.i(tag, "handleSetBluetoothAudio not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSetCameraFlip(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleSetCameraFlip not processed");
return currentState;
@ -430,16 +432,6 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleBluetoothChange(@NonNull WebRtcServiceState currentState, boolean available) {
Log.i(tag, "handleBluetoothChange not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleWiredHeadsetChange(@NonNull WebRtcServiceState currentState, boolean present) {
Log.i(tag, "handleWiredHeadsetChange not processed");
return currentState;
}
public @NonNull WebRtcServiceState handleCameraSwitchCompleted(@NonNull WebRtcServiceState currentState, @NonNull CameraState newCameraState) {
Log.i(tag, "handleCameraSwitchCompleted not processed");
return currentState;
@ -564,7 +556,6 @@ public abstract class WebRtcActionProcessor {
(activePeer.getState() == CallState.CONNECTED);
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService();
@ -574,7 +565,6 @@ public abstract class WebRtcActionProcessor {
.activePeer(null)
.commit()
.changeLocalDeviceState()
.wantsBluetooth(false)
.commit()
.actionProcessor(currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED ? new DisconnectingCallActionProcessor(webRtcInteractor) : new IdleActionProcessor(webRtcInteractor))
.terminate()
@ -723,7 +713,6 @@ public abstract class WebRtcActionProcessor {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
boolean playDisconnectSound = currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED;
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService();

Wyświetl plik

@ -6,10 +6,8 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
@ -25,16 +23,18 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager;
import org.thoughtcrime.securesms.webrtc.audio.BluetoothStateManager;
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCommand;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import java.util.Objects;
import java.util.Set;
/**
* Provide a foreground service for {@link SignalCallManager} to leverage to run in the background when necessary. Also
* provides devices listeners needed for during a call (i.e., bluetooth, power button).
*/
public final class WebRtcCallService extends Service implements BluetoothStateManager.BluetoothStateListener {
public final class WebRtcCallService extends Service implements SignalAudioManager.EventListener {
private static final String TAG = Log.tag(WebRtcCallService.class);
@ -42,23 +42,23 @@ public final class WebRtcCallService extends Service implements BluetoothStateMa
private static final String ACTION_STOP = "STOP";
private static final String ACTION_DENY_CALL = "DENY_CALL";
private static final String ACTION_LOCAL_HANGUP = "LOCAL_HANGUP";
private static final String ACTION_WANTS_BLUETOOTH = "WANTS_BLUETOOTH";
private static final String ACTION_CHANGE_POWER_BUTTON = "CHANGE_POWER_BUTTON";
private static final String ACTION_SEND_AUDIO_COMMAND = "SEND_AUDIO_COMMAND";
private static final String EXTRA_UPDATE_TYPE = "UPDATE_TYPE";
private static final String EXTRA_RECIPIENT_ID = "RECIPIENT_ID";
private static final String EXTRA_ENABLED = "ENABLED";
private static final String EXTRA_UPDATE_TYPE = "UPDATE_TYPE";
private static final String EXTRA_RECIPIENT_ID = "RECIPIENT_ID";
private static final String EXTRA_ENABLED = "ENABLED";
private static final String EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND";
private static final int INVALID_NOTIFICATION_ID = -1;
private SignalCallManager callManager;
private WiredHeadsetStateReceiver wiredHeadsetStateReceiver;
private NetworkReceiver networkReceiver;
private PowerButtonReceiver powerButtonReceiver;
private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager;
private PhoneStateListener hangUpRtcOnDeviceCallAnswered;
private BluetoothStateManager bluetoothStateManager;
private SignalAudioManager signalAudioManager;
private int lastNotificationId;
private Notification lastNotification;
@ -86,11 +86,10 @@ public final class WebRtcCallService extends Service implements BluetoothStateMa
return new Intent(context, WebRtcCallService.class).setAction(ACTION_LOCAL_HANGUP);
}
public static void setWantsBluetoothConnection(@NonNull Context context, boolean enabled) {
public static void sendAudioManagerCommand(@NonNull Context context, @NonNull AudioManagerCommand command) {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(ACTION_WANTS_BLUETOOTH)
.putExtra(EXTRA_ENABLED, enabled);
intent.setAction(ACTION_SEND_AUDIO_COMMAND)
.putExtra(EXTRA_AUDIO_COMMAND, command);
ContextCompat.startForegroundService(context, intent);
}
@ -107,12 +106,11 @@ public final class WebRtcCallService extends Service implements BluetoothStateMa
Log.v(TAG, "onCreate");
super.onCreate();
this.callManager = ApplicationDependencies.getSignalCallManager();
this.bluetoothStateManager = new BluetoothStateManager(this, this);
this.signalAudioManager = new SignalAudioManager(this, this);
this.hangUpRtcOnDeviceCallAnswered = new HangUpRtcOnPstnCallAnsweredListener();
this.lastNotificationId = INVALID_NOTIFICATION_ID;
registerUncaughtExceptionHandler();
registerWiredHeadsetStateReceiver();
registerNetworkReceiver();
TelephonyUtil.getManager(this)
@ -128,13 +126,8 @@ public final class WebRtcCallService extends Service implements BluetoothStateMa
uncaughtExceptionHandlerManager.unregister();
}
if (bluetoothStateManager != null) {
bluetoothStateManager.onDestroy();
}
if (wiredHeadsetStateReceiver != null) {
unregisterReceiver(wiredHeadsetStateReceiver);
wiredHeadsetStateReceiver = null;
if (signalAudioManager != null) {
signalAudioManager.shutdown();
}
unregisterNetworkReceiver();
@ -157,11 +150,9 @@ public final class WebRtcCallService extends Service implements BluetoothStateMa
setCallInProgressNotification(intent.getIntExtra(EXTRA_UPDATE_TYPE, 0),
Objects.requireNonNull(intent.getParcelableExtra(EXTRA_RECIPIENT_ID)));
return START_STICKY;
case ACTION_WANTS_BLUETOOTH:
case ACTION_SEND_AUDIO_COMMAND:
setCallNotification();
if (bluetoothStateManager != null) {
bluetoothStateManager.setWantsConnection(intent.getBooleanExtra(EXTRA_ENABLED, false));
}
signalAudioManager.handleCommand(Objects.requireNonNull(intent.getParcelableExtra(EXTRA_AUDIO_COMMAND)));
return START_STICKY;
case ACTION_CHANGE_POWER_BUTTON:
setCallNotification();
@ -215,20 +206,6 @@ public final class WebRtcCallService extends Service implements BluetoothStateMa
uncaughtExceptionHandlerManager.registerHandler(new ProximityLockRelease(callManager.getLockManager()));
}
private void registerWiredHeadsetStateReceiver() {
wiredHeadsetStateReceiver = new WiredHeadsetStateReceiver();
String action;
if (Build.VERSION.SDK_INT >= 21) {
action = AudioManager.ACTION_HEADSET_PLUG;
} else {
action = Intent.ACTION_HEADSET_PLUG;
}
registerReceiver(wiredHeadsetStateReceiver, new IntentFilter(action));
}
private void registerNetworkReceiver() {
if (networkReceiver == null) {
networkReceiver = new NetworkReceiver();
@ -267,8 +244,8 @@ public final class WebRtcCallService extends Service implements BluetoothStateMa
}
@Override
public void onBluetoothStateChanged(boolean isAvailable) {
callManager.bluetoothChange(isAvailable);
public void onAudioDeviceChanged(@NonNull SignalAudioManager.AudioDevice activeDevice, @NonNull Set<SignalAudioManager.AudioDevice> availableDevices) {
callManager.onAudioDeviceChanged(activeDevice, availableDevices);
}
private class HangUpRtcOnPstnCallAnsweredListener extends PhoneStateListener {
@ -286,15 +263,6 @@ public final class WebRtcCallService extends Service implements BluetoothStateMa
}
}
private static class WiredHeadsetStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
int state = intent.getIntExtra("state", -1);
ApplicationDependencies.getSignalCallManager().wiredHeadsetChange(state != 0);
}
}
private static class NetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {

Wyświetl plik

@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCommand;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
@ -32,7 +32,6 @@ public class WebRtcInteractor {
@NonNull private final Context context;
@NonNull private final SignalCallManager signalCallManager;
@NonNull private final LockManager lockManager;
@NonNull private final SignalAudioManager audioManager;
@NonNull private final CameraEventListener cameraEventListener;
@NonNull private final GroupCall.Observer groupCallObserver;
@NonNull private final AppForegroundObserver.Listener foregroundListener;
@ -40,7 +39,6 @@ public class WebRtcInteractor {
public WebRtcInteractor(@NonNull Context context,
@NonNull SignalCallManager signalCallManager,
@NonNull LockManager lockManager,
@NonNull SignalAudioManager audioManager,
@NonNull CameraEventListener cameraEventListener,
@NonNull GroupCall.Observer groupCallObserver,
@NonNull AppForegroundObserver.Listener foregroundListener)
@ -48,7 +46,6 @@ public class WebRtcInteractor {
this.context = context;
this.signalCallManager = signalCallManager;
this.lockManager = lockManager;
this.audioManager = audioManager;
this.cameraEventListener = cameraEventListener;
this.groupCallObserver = groupCallObserver;
this.foregroundListener = foregroundListener;
@ -74,10 +71,6 @@ public class WebRtcInteractor {
return foregroundListener;
}
void setWantsBluetoothConnection(boolean enabled) {
WebRtcCallService.setWantsBluetoothConnection(context, enabled);
}
void updatePhoneState(@NonNull LockManager.PhoneState phoneState) {
lockManager.updatePhoneState(phoneState);
}
@ -131,27 +124,35 @@ public class WebRtcInteractor {
}
void silenceIncomingRinger() {
audioManager.silenceIncomingRinger();
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SilenceIncomingRinger());
}
void initializeAudioForCall() {
audioManager.initializeAudioForCall();
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.Initialize());
}
void startIncomingRinger(@Nullable Uri ringtoneUri, boolean vibrate) {
audioManager.startIncomingRinger(ringtoneUri, vibrate);
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.StartIncomingRinger(ringtoneUri, vibrate));
}
void startOutgoingRinger() {
audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING);
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.StartOutgoingRinger());
}
void stopAudio(boolean playDisconnect) {
audioManager.stop(playDisconnect);
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.Stop(playDisconnect));
}
void startAudioCommunication(boolean preserveSpeakerphone) {
audioManager.startCommunication(preserveSpeakerphone);
void startAudioCommunication() {
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.Start());
}
public void setUserAudioDevice(@NonNull SignalAudioManager.AudioDevice userDevice) {
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetUserDevice(userDevice));
}
public void setDefaultAudioDevice(@NonNull SignalAudioManager.AudioDevice userDevice, boolean clearUserEarpieceSelection) {
WebRtcCallService.sendAudioManagerCommand(context, new AudioManagerCommand.SetDefaultDevice(userDevice, clearUserEarpieceSelection));
}
void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo groupCallRingCheckInfo) {

Wyświetl plik

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.content.Context;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -9,8 +8,11 @@ import androidx.annotation.Nullable;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.PeekInfo;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
@ -32,7 +34,7 @@ public final class WebRtcUtil {
}
public static @NonNull LockManager.PhoneState getInCallPhoneState(@NonNull Context context) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
AudioManagerCompat audioManager = ApplicationDependencies.getAndroidCallAudioManager();
if (audioManager.isSpeakerphoneOn() || audioManager.isBluetoothScoOn() || audioManager.isWiredHeadsetOn()) {
return LockManager.PhoneState.IN_HANDS_FREE_CALL;
} else {
@ -72,17 +74,15 @@ public final class WebRtcUtil {
return OpaqueMessage.Urgency.DROPPABLE;
}
public static void enableSpeakerPhoneIfNeeded(@NonNull Context context, boolean enable) {
if (!enable) {
public static void enableSpeakerPhoneIfNeeded(@NonNull WebRtcInteractor webRtcInteractor, WebRtcServiceState currentState) {
if (!currentState.getLocalDeviceState().getCameraState().isEnabled()) {
return;
}
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
//noinspection deprecation
boolean shouldEnable = !(androidAudioManager.isSpeakerphoneOn() || androidAudioManager.isBluetoothScoOn() || androidAudioManager.isWiredHeadsetOn());
if (shouldEnable) {
androidAudioManager.setSpeakerphoneOn(true);
if (currentState.getLocalDeviceState().getActiveDevice() == SignalAudioManager.AudioDevice.EARPIECE ||
currentState.getLocalDeviceState().getActiveDevice() == SignalAudioManager.AudioDevice.NONE)
{
webRtcInteractor.setDefaultAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE, true);
}
}

Wyświetl plik

@ -1,72 +0,0 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.ringrtc.CameraState;
/**
* Local device specific state.
*/
public final class LocalDeviceState {
CameraState cameraState;
boolean microphoneEnabled;
boolean bluetoothAvailable;
boolean wantsBluetooth;
Orientation orientation;
boolean isLandscapeEnabled;
Orientation deviceOrientation;
LocalDeviceState() {
this(CameraState.UNKNOWN, true, false, false, Orientation.PORTRAIT_BOTTOM_EDGE, false, Orientation.PORTRAIT_BOTTOM_EDGE);
}
LocalDeviceState(@NonNull LocalDeviceState toCopy) {
this(toCopy.cameraState, toCopy.microphoneEnabled, toCopy.bluetoothAvailable, toCopy.wantsBluetooth, toCopy.orientation, toCopy.isLandscapeEnabled, toCopy.deviceOrientation);
}
LocalDeviceState(@NonNull CameraState cameraState,
boolean microphoneEnabled,
boolean bluetoothAvailable,
boolean wantsBluetooth,
@NonNull Orientation orientation,
boolean isLandscapeEnabled,
@NonNull Orientation deviceOrientation)
{
this.cameraState = cameraState;
this.microphoneEnabled = microphoneEnabled;
this.bluetoothAvailable = bluetoothAvailable;
this.wantsBluetooth = wantsBluetooth;
this.orientation = orientation;
this.isLandscapeEnabled = isLandscapeEnabled;
this.deviceOrientation = deviceOrientation;
}
public @NonNull CameraState getCameraState() {
return cameraState;
}
public boolean isMicrophoneEnabled() {
return microphoneEnabled;
}
public boolean isBluetoothAvailable() {
return bluetoothAvailable;
}
public boolean wantsBluetooth() {
return wantsBluetooth;
}
public @NonNull Orientation getOrientation() {
return orientation;
}
public boolean isLandscapeEnabled() {
return isLandscapeEnabled;
}
public @NonNull Orientation getDeviceOrientation() {
return deviceOrientation;
}
}

Wyświetl plik

@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.service.webrtc.state
import org.thoughtcrime.securesms.components.sensors.Orientation
import org.thoughtcrime.securesms.ringrtc.CameraState
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
/**
* Local device specific state.
*/
data class LocalDeviceState constructor(
var cameraState: CameraState = CameraState.UNKNOWN,
var isMicrophoneEnabled: Boolean = true,
var orientation: Orientation = Orientation.PORTRAIT_BOTTOM_EDGE,
var isLandscapeEnabled: Boolean = false,
var deviceOrientation: Orientation = Orientation.PORTRAIT_BOTTOM_EDGE,
var activeDevice: SignalAudioManager.AudioDevice = SignalAudioManager.AudioDevice.NONE,
var availableDevices: Set<SignalAudioManager.AudioDevice> = emptySet()
) {
fun duplicate(): LocalDeviceState {
return copy()
}
}

Wyświetl plik

@ -27,7 +27,7 @@ public final class WebRtcServiceState {
this.actionProcessor = toCopy.actionProcessor;
this.callSetupState = toCopy.callSetupState.duplicate();
this.callInfoState = new CallInfoState(toCopy.callInfoState);
this.localDeviceState = new LocalDeviceState(toCopy.localDeviceState);
this.localDeviceState = toCopy.localDeviceState.duplicate();
this.videoState = new VideoState(toCopy.videoState);
}

Wyświetl plik

@ -18,8 +18,10 @@ import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import java.util.Collection;
import java.util.Set;
/**
* Builder that creates a new {@link WebRtcServiceState} from an existing one and allows
@ -73,7 +75,7 @@ public class WebRtcServiceStateBuilder {
private LocalDeviceState toBuild;
public LocalDeviceStateBuilder() {
toBuild = new LocalDeviceState(WebRtcServiceStateBuilder.this.toBuild.localDeviceState);
toBuild = WebRtcServiceStateBuilder.this.toBuild.localDeviceState.duplicate();
}
public @NonNull WebRtcServiceStateBuilder commit() {
@ -87,37 +89,37 @@ public class WebRtcServiceStateBuilder {
}
public @NonNull LocalDeviceStateBuilder cameraState(@NonNull CameraState cameraState) {
toBuild.cameraState = cameraState;
toBuild.setCameraState(cameraState);
return this;
}
public @NonNull LocalDeviceStateBuilder isMicrophoneEnabled(boolean enabled) {
toBuild.microphoneEnabled = enabled;
return this;
}
public @NonNull LocalDeviceStateBuilder isBluetoothAvailable(boolean available) {
toBuild.bluetoothAvailable = available;
return this;
}
public @NonNull LocalDeviceStateBuilder wantsBluetooth(boolean wantsBluetooth) {
toBuild.wantsBluetooth = wantsBluetooth;
toBuild.setMicrophoneEnabled(enabled);
return this;
}
public @NonNull LocalDeviceStateBuilder setOrientation(@NonNull Orientation orientation) {
toBuild.orientation = orientation;
toBuild.setOrientation(orientation);
return this;
}
public @NonNull LocalDeviceStateBuilder setLandscapeEnabled(boolean isLandscapeEnabled) {
toBuild.isLandscapeEnabled = isLandscapeEnabled;
toBuild.setLandscapeEnabled(isLandscapeEnabled);
return this;
}
public @NonNull LocalDeviceStateBuilder setDeviceOrientation(@NonNull Orientation deviceOrientation) {
toBuild.deviceOrientation = deviceOrientation;
toBuild.setDeviceOrientation(deviceOrientation);
return this;
}
public @NonNull LocalDeviceStateBuilder setActiveDevice(@NonNull SignalAudioManager.AudioDevice audioDevice) {
toBuild.setActiveDevice(audioDevice);
return this;
}
public @NonNull LocalDeviceStateBuilder setAvailableDevices(@NonNull Set<SignalAudioManager.AudioDevice> availableDevices) {
toBuild.setAvailableDevices(availableDevices);
return this;
}
}

Wyświetl plik

@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.util
import android.content.BroadcastReceiver
import android.content.Context
fun Context.safeUnregisterReceiver(receiver: BroadcastReceiver?) {
if (receiver == null) {
return
}
try {
unregisterReceiver(receiver)
} catch (e: IllegalArgumentException) {
}
}

Wyświetl plik

@ -0,0 +1,109 @@
package org.thoughtcrime.securesms.webrtc.audio
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import org.thoughtcrime.securesms.util.ParcelUtil
/**
* Commands that can be issued to [SignalAudioManager] to perform various tasks.
*
* Additional context: The audio management is tied closely with the Android audio and thus benefits from being
* tied to the [org.thoughtcrime.securesms.service.webrtc.WebRtcCallService] lifecycle. Because of this, all
* calls have to go through an intent to the service and this allows one entry point for that but multiple
* operations.
*/
sealed class AudioManagerCommand : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) = Unit
override fun describeContents(): Int = 0
class Initialize : AudioManagerCommand() {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<Initialize> = ParcelCheat { Initialize() }
}
}
class StartIncomingRinger(val ringtoneUri: Uri, val vibrate: Boolean) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(ringtoneUri, flags)
ParcelUtil.writeBoolean(parcel, vibrate)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<StartIncomingRinger> = ParcelCheat { parcel ->
StartIncomingRinger(
ringtoneUri = parcel.readParcelable(Uri::class.java.classLoader)!!,
vibrate = ParcelUtil.readBoolean(parcel)
)
}
}
}
class StartOutgoingRinger : AudioManagerCommand() {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<StartOutgoingRinger> = ParcelCheat { StartOutgoingRinger() }
}
}
class SilenceIncomingRinger : AudioManagerCommand() {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<SilenceIncomingRinger> = ParcelCheat { SilenceIncomingRinger() }
}
}
class Start : AudioManagerCommand() {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<Start> = ParcelCheat { Start() }
}
}
class Stop(val playDisconnect: Boolean) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) {
ParcelUtil.writeBoolean(parcel, playDisconnect)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<Stop> = ParcelCheat { Stop(ParcelUtil.readBoolean(it)) }
}
}
class SetUserDevice(val device: SignalAudioManager.AudioDevice) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeSerializable(device)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<SetUserDevice> = ParcelCheat { SetUserDevice(it.readSerializable() as SignalAudioManager.AudioDevice) }
}
}
class SetDefaultDevice(val device: SignalAudioManager.AudioDevice, val clearUserEarpieceSelection: Boolean) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeSerializable(device)
ParcelUtil.writeBoolean(parcel, clearUserEarpieceSelection)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<SetDefaultDevice> = ParcelCheat { parcel ->
SetDefaultDevice(
device = parcel.readSerializable() as SignalAudioManager.AudioDevice,
clearUserEarpieceSelection = ParcelUtil.readBoolean(parcel)
)
}
}
}
class ParcelCheat<T>(private val createFrom: (Parcel) -> T) : Parcelable.Creator<T> {
override fun createFromParcel(parcel: Parcel): T = createFrom(parcel)
override fun newArray(size: Int): Array<T?> = throw UnsupportedOperationException()
}
}

Wyświetl plik

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.webrtc.audio;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.SoundPool;
@ -30,6 +33,88 @@ public abstract class AudioManagerCompat {
audioManager = ServiceUtil.getAudioManager(context);
}
public boolean isBluetoothScoAvailableOffCall() {
return audioManager.isBluetoothScoAvailableOffCall();
}
public void startBluetoothSco() {
audioManager.startBluetoothSco();
}
public void stopBluetoothSco() {
audioManager.stopBluetoothSco();
}
public boolean isBluetoothScoOn() {
return audioManager.isBluetoothScoOn();
}
public void setBluetoothScoOn(boolean on) {
audioManager.setBluetoothScoOn(on);
}
public int getMode() {
return audioManager.getMode();
}
public void setMode(int modeInCommunication) {
audioManager.setMode(modeInCommunication);
}
public boolean isSpeakerphoneOn() {
return audioManager.isSpeakerphoneOn();
}
public void setSpeakerphoneOn(boolean on) {
audioManager.setSpeakerphoneOn(on);
}
public boolean isMicrophoneMute() {
return audioManager.isMicrophoneMute();
}
public void setMicrophoneMute(boolean on) {
audioManager.setMicrophoneMute(on);
}
public boolean hasEarpiece(@NonNull Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
}
@SuppressLint("WrongConstant")
public boolean isWiredHeadsetOn() {
if (Build.VERSION.SDK_INT < 23) {
//noinspection deprecation
return audioManager.isWiredHeadsetOn();
} else {
AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
for (AudioDeviceInfo device : devices) {
final int type = device.getType();
if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
return true;
} else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
return true;
}
}
return false;
}
}
public float ringVolumeWithMinimum() {
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
float volume = logVolume(currentVolume, maxVolume);
float minVolume = logVolume(15, 100);
return Math.max(volume, minVolume);
}
private static float logVolume(int volume, int maxVolume) {
if (maxVolume == 0 || volume > maxVolume) {
return 0.5f;
}
return (float) (1 - (Math.log(maxVolume + 1 - volume) / Math.log(maxVolume + 1)));
}
abstract public SoundPool createSoundPool();
abstract public void requestCallAudioFocus();
abstract public void abandonCallAudioFocus();

Wyświetl plik

@ -1,257 +0,0 @@
package org.thoughtcrime.securesms.webrtc.audio;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Note: We will need to start handling new permissions once we move to target API 31
*/
@SuppressLint("MissingPermission")
public class BluetoothStateManager {
private static final String TAG = Log.tag(BluetoothStateManager.class);
private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
private enum ScoConnection {
DISCONNECTED,
IN_PROGRESS,
CONNECTED
}
private final Object LOCK = new Object();
private final Context context;
private final BluetoothAdapter bluetoothAdapter;
private BluetoothScoReceiver bluetoothScoReceiver;
private BluetoothConnectionReceiver bluetoothConnectionReceiver;
private final BluetoothStateListener listener;
private final AtomicBoolean destroyed;
private volatile ScoConnection scoConnection = ScoConnection.DISCONNECTED;
private int scoConnectionAttempts = 0;
private BluetoothHeadset bluetoothHeadset = null;
private boolean wantsConnection = false;
public BluetoothStateManager(@NonNull Context context, @Nullable BluetoothStateListener listener) {
this.context = context.getApplicationContext();
BluetoothAdapter localAdapter = BluetoothAdapter.getDefaultAdapter();
if (localAdapter == null) {
this.bluetoothAdapter = null;
this.listener = null;
this.destroyed = new AtomicBoolean(true);
return;
}
this.bluetoothAdapter = localAdapter;
this.bluetoothScoReceiver = new BluetoothScoReceiver();
this.bluetoothConnectionReceiver = new BluetoothConnectionReceiver();
this.listener = listener;
this.destroyed = new AtomicBoolean(false);
requestHeadsetProxyProfile();
this.context.registerReceiver(bluetoothConnectionReceiver, new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED));
Intent sticky = this.context.registerReceiver(bluetoothScoReceiver, new IntentFilter(getScoChangeIntent()));
if (sticky != null) {
bluetoothScoReceiver.onReceive(context, sticky);
}
handleBluetoothStateChange();
}
public void onDestroy() {
destroyed.set(true);
if (bluetoothHeadset != null && bluetoothAdapter != null) {
this.bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
}
if (bluetoothConnectionReceiver != null) {
context.unregisterReceiver(bluetoothConnectionReceiver);
bluetoothConnectionReceiver = null;
}
if (bluetoothScoReceiver != null) {
context.unregisterReceiver(bluetoothScoReceiver);
bluetoothScoReceiver = null;
}
this.bluetoothHeadset = null;
}
public void setWantsConnection(boolean enabled) {
synchronized (LOCK) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
this.wantsConnection = enabled;
if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
if (scoConnectionAttempts > MAX_SCO_CONNECTION_ATTEMPTS) {
Log.w(TAG, "We've already attempted to start SCO too many times. Won't try again.");
} else {
scoConnectionAttempts++;
audioManager.startBluetoothSco();
scoConnection = ScoConnection.IN_PROGRESS;
}
} else if (!wantsConnection && scoConnection == ScoConnection.CONNECTED) {
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
scoConnection = ScoConnection.DISCONNECTED;
} else if (!wantsConnection && scoConnection == ScoConnection.IN_PROGRESS) {
audioManager.stopBluetoothSco();
scoConnection = ScoConnection.DISCONNECTED;
}
}
}
private void handleBluetoothStateChange() {
if (!destroyed.get()) {
boolean isBluetoothAvailable = isBluetoothAvailable();
if (!isBluetoothAvailable) {
setWantsConnection(false);
}
if (listener != null) {
listener.onBluetoothStateChanged(isBluetoothAvailable);
}
}
}
private boolean isBluetoothAvailable() {
try {
synchronized (LOCK) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) return false;
if (!audioManager.isBluetoothScoAvailableOffCall()) return false;
return bluetoothHeadset != null && !bluetoothHeadset.getConnectedDevices().isEmpty();
}
} catch (Exception e) {
Log.w(TAG, e);
return false;
}
}
private String getScoChangeIntent() {
return AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED;
}
private void requestHeadsetProxyProfile() {
this.bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (destroyed.get()) {
Log.w(TAG, "Got bluetooth profile event after the service was destroyed. Ignoring.");
return;
}
if (profile == BluetoothProfile.HEADSET) {
synchronized (LOCK) {
bluetoothHeadset = (BluetoothHeadset) proxy;
}
Intent sticky = context.registerReceiver(null, new IntentFilter(getScoChangeIntent()));
bluetoothScoReceiver.onReceive(context, sticky);
synchronized (LOCK) {
if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
audioManager.startBluetoothSco();
scoConnection = ScoConnection.IN_PROGRESS;
}
}
handleBluetoothStateChange();
}
}
@Override
public void onServiceDisconnected(int profile) {
Log.i(TAG, "onServiceDisconnected");
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = null;
handleBluetoothStateChange();
}
}
}, BluetoothProfile.HEADSET);
}
private class BluetoothScoReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) return;
Log.i(TAG, "onReceive");
synchronized (LOCK) {
if (getScoChangeIntent().equals(intent.getAction())) {
int status = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, AudioManager.SCO_AUDIO_STATE_ERROR);
if (status == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
if (bluetoothHeadset != null) {
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
for (BluetoothDevice device : devices) {
if (bluetoothHeadset.isAudioConnected(device)) {
scoConnection = ScoConnection.CONNECTED;
scoConnectionAttempts = 0;
if (wantsConnection) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
audioManager.setBluetoothScoOn(true);
}
}
}
}
} else if (status == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) {
setWantsConnection(false);
}
}
}
handleBluetoothStateChange();
}
}
private class BluetoothConnectionReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive");
if (intent.getAction().equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
if (state == BluetoothHeadset.STATE_CONNECTED) {
scoConnectionAttempts = 0;
}
}
handleBluetoothStateChange();
}
}
public interface BluetoothStateListener {
void onBluetoothStateChanged(boolean isAvailable);
}
}

Wyświetl plik

@ -51,6 +51,8 @@ public class IncomingRinger {
if (shouldVibrate(context, player, ringerMode, vibrate)) {
Log.i(TAG, "Starting vibration");
vibrator.vibrate(VIBRATE_PATTERN, 1);
} else {
Log.i(TAG, "Skipping vibration");
}
if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {

Wyświetl plik

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.webrtc.audio
import android.os.Handler
import android.os.Looper
/**
* Handler to run all audio/bluetooth operations. Provides current thread
* assertion for enforcing use of the handler when necessary.
*/
class SignalAudioHandler(looper: Looper) : Handler(looper) {
fun assertHandlerThread() {
if (!isOnHandler()) {
throw AssertionError("Must run on audio handler thread.")
}
}
fun isOnHandler(): Boolean {
return Looper.myLooper() == looper
}
}

Wyświetl plik

@ -1,116 +0,0 @@
package org.thoughtcrime.securesms.webrtc.audio;
import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil;
public class SignalAudioManager {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(SignalAudioManager.class);
private final Context context;
private final IncomingRinger incomingRinger;
private final OutgoingRinger outgoingRinger;
private final SoundPool soundPool;
private final int connectedSoundId;
private final int disconnectedSoundId;
private final AudioManagerCompat audioManagerCompat;
public SignalAudioManager(@NonNull Context context) {
this.context = context.getApplicationContext();
this.incomingRinger = new IncomingRinger(context);
this.outgoingRinger = new OutgoingRinger(context);
this.audioManagerCompat = AudioManagerCompat.create(context);
this.soundPool = audioManagerCompat.createSoundPool();
this.connectedSoundId = this.soundPool.load(context, R.raw.webrtc_completed, 1);
this.disconnectedSoundId = this.soundPool.load(context, R.raw.webrtc_disconnected, 1);
}
public void initializeAudioForCall() {
audioManagerCompat.requestCallAudioFocus();
}
public void startIncomingRinger(@Nullable Uri ringtoneUri, boolean vibrate) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
boolean speaker = !audioManager.isWiredHeadsetOn() && !audioManager.isBluetoothScoOn();
audioManager.setMode(AudioManager.MODE_RINGTONE);
audioManager.setMicrophoneMute(false);
audioManager.setSpeakerphoneOn(speaker);
incomingRinger.start(ringtoneUri, vibrate);
}
public void startOutgoingRinger(OutgoingRinger.Type type) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
audioManager.setMicrophoneMute(false);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
outgoingRinger.start(type);
}
public void silenceIncomingRinger() {
incomingRinger.stop();
}
public void startCommunication(boolean preserveSpeakerphone) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
incomingRinger.stop();
outgoingRinger.stop();
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
if (!preserveSpeakerphone) {
audioManager.setSpeakerphoneOn(false);
}
float volume = ringVolumeWithMinimum(audioManager);
soundPool.play(connectedSoundId, volume, volume, 0, 0, 1.0f);
}
public void stop(boolean playDisconnected) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
incomingRinger.stop();
outgoingRinger.stop();
if (playDisconnected) {
float volume = ringVolumeWithMinimum(audioManager);
soundPool.play(disconnectedSoundId, volume, volume, 0, 0, 1.0f);
}
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManagerCompat.abandonCallAudioFocus();
}
private static float ringVolumeWithMinimum(@NonNull AudioManager audioManager) {
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
float volume = logVolume(currentVolume, maxVolume);
float minVolume = logVolume(15, 100);
return Math.max(volume, minVolume);
}
private static float logVolume(int volume, int maxVolume) {
if (maxVolume == 0 || volume > maxVolume) {
return 0.5f;
}
return (float) (1 - (Math.log(maxVolume + 1 - volume) / Math.log(maxVolume + 1)));
}
}

Wyświetl plik

@ -0,0 +1,373 @@
package org.thoughtcrime.securesms.webrtc.audio
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.media.SoundPool
import android.net.Uri
import android.os.Build
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
import org.whispersystems.libsignal.util.guava.Preconditions
private val TAG = Log.tag(SignalAudioManager::class.java)
/**
* Manage all audio and bluetooth routing for calling. Primarily, operates by maintaining a list
* of available devices (wired, speaker, bluetooth, earpiece) and then using a state machine to determine
* which device to use. Inputs into the decision include the [defaultAudioDevice] (set based on if audio
* only or video call) and [userSelectedAudioDevice] (set by user interaction with UI). [autoSwitchToWiredHeadset]
* and [autoSwitchToBluetooth] also impact the decision by forcing the user selection to the respective device
* when initially discovered. If the user switches to another device while bluetooth or wired headset are
* connected, the system will not auto switch back until the audio device is disconnected and reconnected.
*
* For example, call starts with speaker, then a bluetooth headset is connected. The audio will automatically
* switch to the headset. The user can then switch back to speaker through a manual interaction. If the
* bluetooth headset is then disconnected, and reconnected, the audio will again automatically switch to
* the bluetooth headset.
*/
class SignalAudioManager(private val context: Context, private val eventListener: EventListener?) {
private var commandAndControlThread = SignalExecutors.getAndStartHandlerThread("call-audio")
private val handler = SignalAudioHandler(commandAndControlThread.looper)
private val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
private val signalBluetoothManager = SignalBluetoothManager(context, this, handler)
private var state: State = State.UNINITIALIZED
private var savedAudioMode = AudioManager.MODE_INVALID
private var savedIsSpeakerPhoneOn = false
private var savedIsMicrophoneMute = false
private var hasWiredHeadset = false
private var autoSwitchToWiredHeadset = true
private var autoSwitchToBluetooth = true
private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE
private var selectedAudioDevice: AudioDevice = AudioDevice.NONE
private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE
private var audioDevices: MutableSet<AudioDevice> = mutableSetOf()
private val soundPool: SoundPool = androidAudioManager.createSoundPool()
private val connectedSoundId = soundPool.load(context, R.raw.webrtc_completed, 1)
private val disconnectedSoundId = soundPool.load(context, R.raw.webrtc_disconnected, 1)
private val incomingRinger = IncomingRinger(context)
private val outgoingRinger = OutgoingRinger(context)
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
fun handleCommand(command: AudioManagerCommand) {
handler.post {
when (command) {
is AudioManagerCommand.Initialize -> initialize()
is AudioManagerCommand.Start -> start()
is AudioManagerCommand.Stop -> stop(command.playDisconnect)
is AudioManagerCommand.SetDefaultDevice -> setDefaultAudioDevice(command.device, command.clearUserEarpieceSelection)
is AudioManagerCommand.SetUserDevice -> selectAudioDevice(command.device)
is AudioManagerCommand.StartIncomingRinger -> startIncomingRinger(command.ringtoneUri, command.vibrate)
is AudioManagerCommand.SilenceIncomingRinger -> silenceIncomingRinger()
is AudioManagerCommand.StartOutgoingRinger -> startOutgoingRinger()
}
}
}
private fun initialize() {
Log.i(TAG, "Initializing audio manager state: $state")
if (state == State.UNINITIALIZED) {
savedAudioMode = androidAudioManager.mode
savedIsSpeakerPhoneOn = androidAudioManager.isSpeakerphoneOn
savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute
hasWiredHeadset = androidAudioManager.isWiredHeadsetOn
androidAudioManager.requestCallAudioFocus()
setMicrophoneMute(false)
audioDevices.clear()
signalBluetoothManager.start()
updateAudioDeviceState()
wiredHeadsetReceiver = WiredHeadsetReceiver()
context.registerReceiver(wiredHeadsetReceiver, IntentFilter(if (Build.VERSION.SDK_INT >= 21) AudioManager.ACTION_HEADSET_PLUG else Intent.ACTION_HEADSET_PLUG))
state = State.PREINITIALIZED
Log.d(TAG, "Initialized")
}
}
private fun start() {
Log.d(TAG, "Starting. state: $state")
if (state == State.RUNNING) {
Log.w(TAG, "Skipping, already active")
return
}
incomingRinger.stop()
outgoingRinger.stop()
state = State.RUNNING
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
val volume: Float = androidAudioManager.ringVolumeWithMinimum()
soundPool.play(connectedSoundId, volume, volume, 0, 0, 1.0f)
Log.d(TAG, "Started")
}
private fun stop(playDisconnect: Boolean) {
Log.d(TAG, "Stopping. state: $state")
if (state == State.UNINITIALIZED) {
Log.i(TAG, "Trying to stop AudioManager in incorrect state: $state")
return
}
incomingRinger.stop()
outgoingRinger.stop()
if (playDisconnect) {
val volume: Float = androidAudioManager.ringVolumeWithMinimum()
soundPool.play(disconnectedSoundId, volume, volume, 0, 0, 1.0f)
}
state = State.UNINITIALIZED
context.safeUnregisterReceiver(wiredHeadsetReceiver)
wiredHeadsetReceiver = null
signalBluetoothManager.stop()
setSpeakerphoneOn(savedIsSpeakerPhoneOn)
setMicrophoneMute(savedIsMicrophoneMute)
androidAudioManager.mode = savedAudioMode
androidAudioManager.abandonCallAudioFocus()
Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams")
Log.d(TAG, "Stopped")
}
fun shutdown() {
handler.post {
stop(false)
if (commandAndControlThread != null) {
Log.i(TAG, "Shutting down command and control")
commandAndControlThread.quitSafely()
commandAndControlThread = null
}
}
}
fun updateAudioDeviceState() {
handler.assertHandlerThread()
Log.i(
TAG,
"updateAudioDeviceState(): " +
"wired: $hasWiredHeadset " +
"bt: ${signalBluetoothManager.state} " +
"available: $audioDevices " +
"selected: $selectedAudioDevice " +
"userSelected: $userSelectedAudioDevice"
)
if (signalBluetoothManager.state.shouldUpdate()) {
signalBluetoothManager.updateDevice()
}
val newAudioDevices = mutableSetOf(AudioDevice.SPEAKER_PHONE)
if (signalBluetoothManager.state.hasDevice()) {
newAudioDevices += AudioDevice.BLUETOOTH
}
if (hasWiredHeadset) {
newAudioDevices += AudioDevice.WIRED_HEADSET
} else {
autoSwitchToWiredHeadset = true
if (androidAudioManager.hasEarpiece(context)) {
newAudioDevices += AudioDevice.EARPIECE
}
}
var audioDeviceSetUpdated = audioDevices != newAudioDevices
audioDevices = newAudioDevices
if (signalBluetoothManager.state == SignalBluetoothManager.State.UNAVAILABLE && userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
userSelectedAudioDevice = AudioDevice.NONE
}
if (hasWiredHeadset && autoSwitchToWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) {
userSelectedAudioDevice = AudioDevice.WIRED_HEADSET
autoSwitchToWiredHeadset = false
}
if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) {
userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE
}
val needBluetoothAudioStart = signalBluetoothManager.state == SignalBluetoothManager.State.AVAILABLE &&
(userSelectedAudioDevice == AudioDevice.NONE || userSelectedAudioDevice == AudioDevice.BLUETOOTH || autoSwitchToBluetooth)
val needBluetoothAudioStop = (signalBluetoothManager.state == SignalBluetoothManager.State.CONNECTED || signalBluetoothManager.state == SignalBluetoothManager.State.CONNECTING) &&
(userSelectedAudioDevice != AudioDevice.NONE && userSelectedAudioDevice != AudioDevice.BLUETOOTH)
if (signalBluetoothManager.state.hasDevice()) {
Log.i(TAG, "Need bluetooth audio: state: ${signalBluetoothManager.state} start: $needBluetoothAudioStart stop: $needBluetoothAudioStop")
}
if (needBluetoothAudioStop) {
signalBluetoothManager.stopScoAudio()
signalBluetoothManager.updateDevice()
}
if (!autoSwitchToBluetooth && signalBluetoothManager.state == SignalBluetoothManager.State.UNAVAILABLE) {
autoSwitchToBluetooth = true
}
if (needBluetoothAudioStart && !needBluetoothAudioStop) {
if (!signalBluetoothManager.startScoAudio()) {
audioDevices.remove(AudioDevice.BLUETOOTH)
audioDeviceSetUpdated = true
}
}
if (autoSwitchToBluetooth && signalBluetoothManager.state == SignalBluetoothManager.State.CONNECTED) {
userSelectedAudioDevice = AudioDevice.BLUETOOTH
autoSwitchToBluetooth = false
}
val newAudioDevice: AudioDevice = when {
audioDevices.contains(userSelectedAudioDevice) -> userSelectedAudioDevice
audioDevices.contains(defaultAudioDevice) -> defaultAudioDevice
else -> AudioDevice.SPEAKER_PHONE
}
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
setAudioDevice(newAudioDevice)
Log.i(TAG, "New device status: available: $audioDevices, selected: $newAudioDevice")
eventListener?.onAudioDeviceChanged(selectedAudioDevice, audioDevices)
}
}
private fun setDefaultAudioDevice(newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
Log.d(TAG, "setDefaultAudioDevice(): currentDefault: $defaultAudioDevice device: $newDefaultDevice clearUser: $clearUserEarpieceSelection")
defaultAudioDevice = when (newDefaultDevice) {
AudioDevice.SPEAKER_PHONE -> newDefaultDevice
AudioDevice.EARPIECE -> {
if (androidAudioManager.hasEarpiece(context)) {
newDefaultDevice
} else {
AudioDevice.SPEAKER_PHONE
}
}
else -> throw AssertionError("Invalid default audio device selection")
}
if (clearUserEarpieceSelection && userSelectedAudioDevice == AudioDevice.EARPIECE) {
Log.d(TAG, "Clearing user setting of earpiece")
userSelectedAudioDevice = AudioDevice.NONE
}
Log.d(TAG, "New default: $defaultAudioDevice userSelected: $userSelectedAudioDevice")
updateAudioDeviceState()
}
private fun selectAudioDevice(device: AudioDevice) {
val actualDevice = if (device == AudioDevice.EARPIECE && audioDevices.contains(AudioDevice.WIRED_HEADSET)) AudioDevice.WIRED_HEADSET else device
Log.d(TAG, "selectAudioDevice(): device: $device actualDevice: $actualDevice")
if (!audioDevices.contains(actualDevice)) {
Log.w(TAG, "Can not select $actualDevice from available $audioDevices")
}
userSelectedAudioDevice = actualDevice
updateAudioDeviceState()
}
private fun setAudioDevice(device: AudioDevice) {
Log.d(TAG, "setAudioDevice(): device: $device")
Preconditions.checkArgument(audioDevices.contains(device))
when (device) {
AudioDevice.SPEAKER_PHONE -> setSpeakerphoneOn(true)
AudioDevice.EARPIECE -> setSpeakerphoneOn(false)
AudioDevice.WIRED_HEADSET -> setSpeakerphoneOn(false)
AudioDevice.BLUETOOTH -> setSpeakerphoneOn(false)
else -> throw AssertionError("Invalid audio device selection")
}
selectedAudioDevice = device
}
private fun setSpeakerphoneOn(on: Boolean) {
if (androidAudioManager.isSpeakerphoneOn != on) {
androidAudioManager.isSpeakerphoneOn = on
}
}
private fun setMicrophoneMute(on: Boolean) {
if (androidAudioManager.isMicrophoneMute != on) {
androidAudioManager.isMicrophoneMute = on
}
}
private fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate")
androidAudioManager.mode = AudioManager.MODE_RINGTONE
setMicrophoneMute(false)
setDefaultAudioDevice(AudioDevice.SPEAKER_PHONE, false)
incomingRinger.start(ringtoneUri, vibrate)
}
private fun silenceIncomingRinger() {
Log.i(TAG, "silenceIncomingRinger():")
incomingRinger.stop()
}
private fun startOutgoingRinger() {
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice")
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
setMicrophoneMute(false)
outgoingRinger.start(OutgoingRinger.Type.RINGING)
}
private fun onWiredHeadsetChange(pluggedIn: Boolean, hasMic: Boolean) {
Log.i(TAG, "onWiredHeadsetChange state: $state plug: $pluggedIn mic: $hasMic")
hasWiredHeadset = pluggedIn
updateAudioDeviceState()
}
private inner class WiredHeadsetReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val pluggedIn = intent.getIntExtra("state", 0) == 1
val hasMic = intent.getIntExtra("microphone", 0) == 1
handler.post { onWiredHeadsetChange(pluggedIn, hasMic) }
}
}
enum class AudioDevice {
SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
}
enum class State {
UNINITIALIZED, PREINITIALIZED, RUNNING
}
interface EventListener {
@JvmSuppressWildcards
fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>)
}
}

Wyświetl plik

@ -0,0 +1,355 @@
package org.thoughtcrime.securesms.webrtc.audio
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
import java.util.concurrent.TimeUnit
/**
* Manages the bluetooth lifecycle with a headset. This class doesn't make any
* determination on if bluetooth should be used. It determines if a device is connected,
* reports that to the [SignalAudioManager], and then handles connecting/disconnecting
* to the device if requested by [SignalAudioManager].
*/
class SignalBluetoothManager(
private val context: Context,
private val audioManager: SignalAudioManager,
private val handler: SignalAudioHandler
) {
var state: State = State.UNINITIALIZED
get() {
handler.assertHandlerThread()
return field
}
private set
private var bluetoothAdapter: BluetoothAdapter? = null
private var bluetoothDevice: BluetoothDevice? = null
private var bluetoothHeadset: BluetoothHeadset? = null
private var scoConnectionAttempts = 0
private val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
private val bluetoothListener = BluetoothServiceListener()
private var bluetoothReceiver: BluetoothHeadsetBroadcastReceiver? = null
private val bluetoothTimeout = { onBluetoothTimeout() }
fun start() {
handler.assertHandlerThread()
Log.d(TAG, "start(): $state")
if (state != State.UNINITIALIZED) {
Log.w(TAG, "Invalid starting state")
return
}
bluetoothHeadset = null
bluetoothDevice = null
scoConnectionAttempts = 0
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
Log.i(TAG, "Device does not support Bluetooth")
return
}
if (!androidAudioManager.isBluetoothScoAvailableOffCall) {
Log.w(TAG, "Bluetooth SCO audio is not available off call")
return
}
if (bluetoothAdapter?.getProfileProxy(context, bluetoothListener, BluetoothProfile.HEADSET) != true) {
Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed")
return
}
val bluetoothHeadsetFilter = IntentFilter().apply {
addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)
}
bluetoothReceiver = BluetoothHeadsetBroadcastReceiver()
context.registerReceiver(bluetoothReceiver, bluetoothHeadsetFilter)
Log.i(TAG, "Headset profile state: ${bluetoothAdapter?.getProfileConnectionState(BluetoothProfile.HEADSET)?.toStateString()}")
Log.i(TAG, "Bluetooth proxy for headset profile has started")
state = State.UNAVAILABLE
}
fun stop() {
handler.assertHandlerThread()
Log.d(TAG, "stop(): state: $state")
if (bluetoothAdapter == null) {
return
}
stopScoAudio()
if (state == State.UNINITIALIZED) {
return
}
context.safeUnregisterReceiver(bluetoothReceiver)
bluetoothReceiver = null
cancelTimer()
if (bluetoothHeadset != null) {
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset)
bluetoothHeadset = null
}
bluetoothAdapter = null
bluetoothDevice = null
state = State.UNINITIALIZED
}
fun startScoAudio(): Boolean {
handler.assertHandlerThread()
Log.i(TAG, "startScoAudio(): $state attempts: $scoConnectionAttempts")
if (scoConnectionAttempts >= MAX_CONNECTION_ATTEMPTS) {
Log.w(TAG, "SCO connection attempts maxed out")
return false
}
if (state != State.AVAILABLE) {
Log.w(TAG, "SCO connection failed as no headset available")
return false
}
state = State.CONNECTING
androidAudioManager.startBluetoothSco()
androidAudioManager.isBluetoothScoOn = true
scoConnectionAttempts++
startTimer()
return true
}
fun stopScoAudio() {
handler.assertHandlerThread()
Log.i(TAG, "stopScoAudio(): $state")
if (state != State.CONNECTING && state != State.CONNECTED) {
return
}
cancelTimer()
androidAudioManager.stopBluetoothSco()
androidAudioManager.isBluetoothScoOn = false
state = State.DISCONNECTING
}
fun updateDevice() {
handler.assertHandlerThread()
Log.d(TAG, "updateDevice(): state: $state")
if (state == State.UNINITIALIZED || bluetoothHeadset == null) {
return
}
val devices: List<BluetoothDevice>? = bluetoothHeadset?.connectedDevices
if (devices == null || devices.isEmpty()) {
bluetoothDevice = null
state = State.UNAVAILABLE
Log.i(TAG, "No connected bluetooth headset")
} else {
bluetoothDevice = devices[0]
state = State.AVAILABLE
Log.i(TAG, "Connected bluetooth headset. headsetState: ${bluetoothHeadset?.getConnectionState(bluetoothDevice)?.toStateString()} scoAudio: ${bluetoothHeadset?.isAudioConnected(bluetoothDevice)}")
}
}
private fun updateAudioDeviceState() {
audioManager.updateAudioDeviceState()
}
private fun startTimer() {
handler.postDelayed(bluetoothTimeout, SCO_TIMEOUT)
}
private fun cancelTimer() {
handler.removeCallbacks(bluetoothTimeout)
}
private fun onBluetoothTimeout() {
Log.i(TAG, "onBluetoothTimeout: state: $state bluetoothHeadset: $bluetoothHeadset")
if (state == State.UNINITIALIZED || bluetoothHeadset == null || state != State.CONNECTING) {
return
}
var scoConnected = false
val devices: List<BluetoothDevice>? = bluetoothHeadset?.connectedDevices
if (devices != null && devices.isNotEmpty()) {
bluetoothDevice = devices[0]
if (bluetoothHeadset?.isAudioConnected(bluetoothDevice) == true) {
Log.d(TAG, "Connected with $bluetoothDevice")
scoConnected = true
} else {
Log.d(TAG, "Not connected with $bluetoothDevice")
}
}
if (scoConnected) {
Log.i(TAG, "Device actually connected and not timed out")
state = State.CONNECTED
scoConnectionAttempts = 0
} else {
Log.w(TAG, "Failed to connect after timeout")
stopScoAudio()
}
updateAudioDeviceState()
}
private fun onServiceConnected(proxy: BluetoothHeadset?) {
bluetoothHeadset = proxy
updateAudioDeviceState()
}
private fun onServiceDisconnected() {
stopScoAudio()
bluetoothHeadset = null
bluetoothDevice = null
state = State.UNAVAILABLE
updateAudioDeviceState()
}
private fun onHeadsetConnectionStateChanged(connectionState: Int) {
Log.i(TAG, "onHeadsetConnectionStateChanged: state: $state connectionState: ${connectionState.toStateString()}")
when (connectionState) {
BluetoothHeadset.STATE_CONNECTED -> {
scoConnectionAttempts = 0
updateAudioDeviceState()
}
BluetoothHeadset.STATE_DISCONNECTED -> {
stopScoAudio()
updateAudioDeviceState()
}
}
}
private fun onAudioStateChanged(audioState: Int, isInitialStateChange: Boolean) {
Log.i(TAG, "onAudioStateChanged: state: $state audioState: ${audioState.toStateString()} initialSticky: $isInitialStateChange")
if (audioState == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
cancelTimer()
if (state === State.CONNECTING) {
Log.d(TAG, "Bluetooth audio SCO is now connected")
state = State.CONNECTED
scoConnectionAttempts = 0
updateAudioDeviceState()
} else {
Log.w(TAG, "Unexpected state ${audioState.toStateString()}")
}
} else if (audioState == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
Log.d(TAG, "Bluetooth audio SCO is now connecting...")
} else if (audioState == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
Log.d(TAG, "Bluetooth audio SCO is now disconnected")
if (isInitialStateChange) {
Log.d(TAG, "Ignore ${audioState.toStateString()} initial sticky broadcast.")
return
}
updateAudioDeviceState()
}
}
private inner class BluetoothServiceListener : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
if (profile == BluetoothProfile.HEADSET) {
handler.post {
if (state != State.UNINITIALIZED) {
onServiceConnected(proxy as? BluetoothHeadset)
}
}
}
}
override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.HEADSET) {
handler.post {
if (state != State.UNINITIALIZED) {
onServiceDisconnected()
}
}
}
}
}
private inner class BluetoothHeadsetBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) {
val connectionState: Int = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED)
handler.post {
if (state != State.UNINITIALIZED) {
onHeadsetConnectionStateChanged(connectionState)
}
}
} else if (intent.action == BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED) {
val connectionState: Int = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED)
handler.post {
if (state != State.UNINITIALIZED) {
onAudioStateChanged(connectionState, isInitialStickyBroadcast)
}
}
}
}
}
enum class State {
UNINITIALIZED,
UNAVAILABLE,
AVAILABLE,
DISCONNECTING,
CONNECTING,
CONNECTED,
ERROR;
fun shouldUpdate(): Boolean {
return this == AVAILABLE || this == UNAVAILABLE || this == DISCONNECTING
}
fun hasDevice(): Boolean {
return this == CONNECTED || this == CONNECTING || this == AVAILABLE
}
}
companion object {
private val TAG = Log.tag(SignalBluetoothManager::class.java)
private val SCO_TIMEOUT = TimeUnit.SECONDS.toMillis(4)
private const val MAX_CONNECTION_ATTEMPTS = 2
}
}
private fun Int.toStateString(): String {
return when (this) {
BluetoothAdapter.STATE_DISCONNECTED -> "DISCONNECTED"
BluetoothAdapter.STATE_CONNECTED -> "CONNECTED"
BluetoothAdapter.STATE_CONNECTING -> "CONNECTING"
BluetoothAdapter.STATE_DISCONNECTING -> "DISCONNECTING"
BluetoothAdapter.STATE_OFF -> "OFF"
BluetoothAdapter.STATE_ON -> "ON"
BluetoothAdapter.STATE_TURNING_OFF -> "TURNING_OFF"
BluetoothAdapter.STATE_TURNING_ON -> "TURNING_ON"
else -> "UNKNOWN"
}
}