kopia lustrzana https://github.com/ryukoposting/Signal-Android
Refactor call audio routing and bluetooth management.
rodzic
6c55916cda
commit
e637f15a43
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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():");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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>)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue