diff --git a/res/drawable-hdpi/ic_bluetooth_white_24dp.png b/res/drawable-hdpi/ic_bluetooth_white_24dp.png
new file mode 100644
index 0000000000..fce1884000
Binary files /dev/null and b/res/drawable-hdpi/ic_bluetooth_white_24dp.png differ
diff --git a/res/drawable-mdpi/ic_bluetooth_white_24dp.png b/res/drawable-mdpi/ic_bluetooth_white_24dp.png
new file mode 100644
index 0000000000..27a8a719f4
Binary files /dev/null and b/res/drawable-mdpi/ic_bluetooth_white_24dp.png differ
diff --git a/res/drawable-xhdpi/ic_bluetooth_white_24dp.png b/res/drawable-xhdpi/ic_bluetooth_white_24dp.png
new file mode 100644
index 0000000000..920f5cae72
Binary files /dev/null and b/res/drawable-xhdpi/ic_bluetooth_white_24dp.png differ
diff --git a/res/drawable-xxhdpi/ic_bluetooth_white_24dp.png b/res/drawable-xxhdpi/ic_bluetooth_white_24dp.png
new file mode 100644
index 0000000000..860c758642
Binary files /dev/null and b/res/drawable-xxhdpi/ic_bluetooth_white_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_bluetooth_white_24dp.png b/res/drawable-xxxhdpi/ic_bluetooth_white_24dp.png
new file mode 100644
index 0000000000..90d8a34133
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_bluetooth_white_24dp.png differ
diff --git a/res/drawable/webrtc_audio_button.xml b/res/drawable/webrtc_audio_button.xml
deleted file mode 100644
index 462a8898b4..0000000000
--- a/res/drawable/webrtc_audio_button.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
- -
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/res/drawable/webrtc_bluetooth_button.xml b/res/drawable/webrtc_bluetooth_button.xml
new file mode 100644
index 0000000000..7cf5ecb363
--- /dev/null
+++ b/res/drawable/webrtc_bluetooth_button.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/webrtc_speaker_button.xml b/res/drawable/webrtc_speaker_button.xml
new file mode 100644
index 0000000000..e756d5c6ed
--- /dev/null
+++ b/res/drawable/webrtc_speaker_button.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/webrtc_call_controls.xml b/res/layout/webrtc_call_controls.xml
index ed9bb0cf88..a4289063a1 100644
--- a/res/layout/webrtc_call_controls.xml
+++ b/res/layout/webrtc_call_controls.xml
@@ -7,12 +7,18 @@
android:orientation="horizontal"
tools:background="@color/textsecure_primary">
-
+
= 11) {
+ speakerButton.setAlpha(1.0f);
+ bluetoothButton.setAlpha(1.0f);
+ videoMuteButton.setAlpha(1.0f);
+ audioMuteButton.setAlpha(1.0f);
+
+ speakerButton.setEnabled(true);
+ bluetoothButton.setEnabled(true);
+ videoMuteButton.setEnabled(true);
+ audioMuteButton.setEnabled(true);
+ } else if (!enabled && Build.VERSION.SDK_INT >= 11) {
+ speakerButton.setAlpha(0.3f);
+ bluetoothButton.setAlpha(0.3f);
+ videoMuteButton.setAlpha(0.3f);
+ audioMuteButton.setAlpha(0.3f);
+
+ speakerButton.setChecked(false);
+ bluetoothButton.setChecked(false);
+ videoMuteButton.setChecked(false);
+ audioMuteButton.setChecked(false);
+
+ speakerButton.setEnabled(false);
+ bluetoothButton.setEnabled(false);
+ videoMuteButton.setEnabled(false);
+ audioMuteButton.setEnabled(false);
+ }
+ }
+
public void displayVideoTooltip(ViewGroup viewGroup) {
if (Build.VERSION.SDK_INT > 15) {
final ToolTipsManager toolTipsManager = new ToolTipsManager();
@@ -135,17 +177,23 @@ public class WebRtcCallControls extends LinearLayout {
}
public void reset() {
- updateAudioButton();
audioMuteButton.setChecked(false);
videoMuteButton.setChecked(false);
+ speakerButton.setChecked(false);
+ bluetoothButton.setChecked(false);
+ bluetoothButton.setVisibility(View.GONE);
}
public static interface MuteButtonListener {
public void onToggle(boolean isMuted);
}
- public static interface AudioButtonListener {
- public void onAudioChange(AudioUtils.AudioMode mode);
+ public static interface SpeakerButtonListener {
+ public void onSpeakerChange(boolean isSpeaker);
+ }
+
+ public static interface BluetoothButtonListener {
+ public void onBluetoothChange(boolean isBluetooth);
}
diff --git a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java
index 99ab5451f8..d35bcc9a95 100644
--- a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java
+++ b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java
@@ -163,8 +163,12 @@ public class WebRtcCallScreen extends FrameLayout implements Recipient.Recipient
this.controls.setVideoMuteButtonListener(listener);
}
- public void setAudioButtonListener(WebRtcCallControls.AudioButtonListener listener) {
- this.controls.setAudioButtonListener(listener);
+ public void setSpeakerButtonListener(WebRtcCallControls.SpeakerButtonListener listener) {
+ this.controls.setSpeakerButtonListener(listener);
+ }
+
+ public void setBluetoothButtonListener(WebRtcCallControls.BluetoothButtonListener listener) {
+ this.controls.setBluetoothButtonListener(listener);
}
public void setHangupButtonListener(final HangupButtonListener listener) {
@@ -184,12 +188,13 @@ public class WebRtcCallScreen extends FrameLayout implements Recipient.Recipient
this.cancelIdentityButton.setOnClickListener(listener);
}
- public void notifyBluetoothChange() {
- this.controls.updateAudioButton();
+ public void updateAudioState(boolean isBluetoothAvailable, boolean isMicrophoneEnabled) {
+ this.controls.updateAudioState(isBluetoothAvailable);
+ this.controls.setMicrophoneEnabled(isMicrophoneEnabled);
}
- public void notifyAudioRoutingChange() {
- this.controls.updateAudioButton();
+ public void setControlsEnabled(boolean enabled) {
+ this.controls.setControlsEnabled(enabled);
}
public void setLocalVideoEnabled(boolean enabled) {
diff --git a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcInCallAudioButton.java b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcInCallAudioButton.java
deleted file mode 100644
index 09a8f98f5b..0000000000
--- a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcInCallAudioButton.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package org.thoughtcrime.securesms.components.webrtc;
-
-import android.content.Context;
-import android.graphics.drawable.LayerDrawable;
-import android.support.v7.widget.PopupMenu;
-import android.util.Log;
-import android.view.MenuItem;
-import android.widget.CompoundButton;
-
-import org.thoughtcrime.redphone.util.AudioUtils;
-import org.thoughtcrime.securesms.R;
-
-import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.DEFAULT;
-import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.HEADSET;
-import static org.thoughtcrime.redphone.util.AudioUtils.AudioMode.SPEAKER;
-
-/**
- * Manages the audio button displayed on the in-call screen
- *
- * The behavior of this button depends on the availability of headset audio, and changes from being a regular
- * toggle button (enabling speakerphone) to bringing up a model dialog that includes speakerphone, bluetooth,
- * and regular audio options.
- *
- * Based on com.android.phone.InCallTouchUI
- *
- * @author Stuart O. Anderson
- */
-public class WebRtcInCallAudioButton {
-
- private static final String TAG = WebRtcInCallAudioButton.class.getName();
-
- private final CompoundButton mAudioButton;
- private boolean headsetAvailable;
- private AudioUtils.AudioMode currentMode;
- private Context context;
- private WebRtcCallControls.AudioButtonListener listener;
-
- public WebRtcInCallAudioButton(CompoundButton audioButton) {
- mAudioButton = audioButton;
-
- currentMode = DEFAULT;
- headsetAvailable = false;
-
- updateView();
- setListener(new WebRtcCallControls.AudioButtonListener() {
- @Override
- public void onAudioChange(AudioUtils.AudioMode mode) {
- //No Action By Default.
- }
- });
- context = audioButton.getContext();
- }
-
- public void setHeadsetAvailable(boolean available) {
- headsetAvailable = available;
- updateView();
- }
-
- public void setAudioMode(AudioUtils.AudioMode newMode) {
- currentMode = newMode;
- updateView();
- }
-
- private void updateView() {
- // The various layers of artwork for this button come from
- // redphone_btn_compound_audio.xmlaudio.xml. Keep track of which layers we want to be
- // visible:
- //
- // - This selector shows the blue bar below the button icon when
- // this button is a toggle *and* it's currently "checked".
- boolean showToggleStateIndication = false;
- //
- // - This is visible if the popup menu is enabled:
- boolean showMoreIndicator = false;
- //
- // - Foreground icons for the button. Exactly one of these is enabled:
- boolean showSpeakerOnIcon = false;
-// boolean showSpeakerOffIcon = false;
- boolean showHandsetIcon = false;
- boolean showHeadsetIcon = false;
-
- boolean speakerOn = currentMode == AudioUtils.AudioMode.SPEAKER;
-
- if (headsetAvailable) {
- mAudioButton.setEnabled(true);
-
- // The audio button is NOT a toggle in this state. (And its
- // setChecked() state is irrelevant since we completely hide the
- // redphone_btn_compound_background layer anyway.)
-
- // Update desired layers:
- showMoreIndicator = true;
- Log.d(TAG, "UI Mode: " + currentMode);
- if (currentMode == AudioUtils.AudioMode.HEADSET) {
- showHeadsetIcon = true;
- } else if (speakerOn) {
- showSpeakerOnIcon = true;
- } else {
- showHandsetIcon = true;
- }
- } else {
- mAudioButton.setEnabled(true);
-
- mAudioButton.setChecked(speakerOn);
- showSpeakerOnIcon = true;
-// showSpeakerOnIcon = speakerOn;
-// showSpeakerOffIcon = !speakerOn;
-
- showToggleStateIndication = true;
- }
-
- final int HIDDEN = 0;
- final int VISIBLE = 255;
-
- LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
-
- layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
- .setAlpha(showToggleStateIndication ? VISIBLE : HIDDEN);
-
- layers.findDrawableByLayerId(R.id.moreIndicatorItem)
- .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
-
- layers.findDrawableByLayerId(R.id.bluetoothItem)
- .setAlpha(showHeadsetIcon ? VISIBLE : HIDDEN);
-
- layers.findDrawableByLayerId(R.id.handsetItem)
- .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
-
- layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
- .setAlpha(showSpeakerOnIcon ? VISIBLE : HIDDEN);
-
-// layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
-// .setAlpha(showSpeakerOffIcon ? VISIBLE : HIDDEN);
-
- mAudioButton.invalidate();
- }
-
- private void log(String msg) {
- Log.d(TAG, msg);
- }
-
- public void setListener(final WebRtcCallControls.AudioButtonListener listener) {
- this.listener = listener;
- mAudioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
- if(headsetAvailable) {
- displayAudioChoiceDialog();
- } else {
- currentMode = b ? AudioUtils.AudioMode.SPEAKER : DEFAULT;
- listener.onAudioChange(currentMode);
- updateView();
- }
- }
- });
- }
-
- private void displayAudioChoiceDialog() {
- Log.w(TAG, "Displaying popup...");
- PopupMenu popupMenu = new PopupMenu(context, mAudioButton);
- popupMenu.getMenuInflater().inflate(R.menu.redphone_audio_popup_menu, popupMenu.getMenu());
- popupMenu.setOnMenuItemClickListener(new AudioRoutingPopupListener());
- popupMenu.show();
- }
-
- private class AudioRoutingPopupListener implements PopupMenu.OnMenuItemClickListener {
- @Override
- public boolean onMenuItemClick(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.handset:
- currentMode = DEFAULT;
- break;
- case R.id.headset:
- currentMode = HEADSET;
- break;
- case R.id.speaker:
- currentMode = SPEAKER;
- break;
- default:
- Log.w(TAG, "Unknown item selected in audio popup menu: " + item.toString());
- }
- Log.d(TAG, "Selected: " + currentMode + " -- " + item.getItemId());
-
- listener.onAudioChange(currentMode);
- updateView();
- return true;
- }
- }
-}
diff --git a/src/org/thoughtcrime/securesms/events/WebRtcViewModel.java b/src/org/thoughtcrime/securesms/events/WebRtcViewModel.java
index a62377d5d6..15787d7ce9 100644
--- a/src/org/thoughtcrime/securesms/events/WebRtcViewModel.java
+++ b/src/org/thoughtcrime/securesms/events/WebRtcViewModel.java
@@ -32,19 +32,30 @@ public class WebRtcViewModel {
private final boolean remoteVideoEnabled;
private final boolean localVideoEnabled;
- public WebRtcViewModel(@NonNull State state, @NonNull Recipient recipient, boolean localVideoEnabled, boolean remoteVideoEnabled) {
- this(state, recipient, null, localVideoEnabled, remoteVideoEnabled);
+ private final boolean isBluetoothAvailable;
+ private final boolean isMicrophoneEnabled;
+
+ public WebRtcViewModel(@NonNull State state, @NonNull Recipient recipient,
+ boolean localVideoEnabled, boolean remoteVideoEnabled,
+ boolean isBluetoothAvailable, boolean isMicrophoneEnabled)
+ {
+ this(state, recipient, null,
+ localVideoEnabled, remoteVideoEnabled,
+ isBluetoothAvailable, isMicrophoneEnabled);
}
public WebRtcViewModel(@NonNull State state, @NonNull Recipient recipient,
@Nullable IdentityKey identityKey,
- boolean localVideoEnabled, boolean remoteVideoEnabled)
+ boolean localVideoEnabled, boolean remoteVideoEnabled,
+ boolean isBluetoothAvailable, boolean isMicrophoneEnabled)
{
- this.state = state;
- this.recipient = recipient;
- this.identityKey = identityKey;
- this.localVideoEnabled = localVideoEnabled;
- this.remoteVideoEnabled = remoteVideoEnabled;
+ this.state = state;
+ this.recipient = recipient;
+ this.identityKey = identityKey;
+ this.localVideoEnabled = localVideoEnabled;
+ this.remoteVideoEnabled = remoteVideoEnabled;
+ this.isBluetoothAvailable = isBluetoothAvailable;
+ this.isMicrophoneEnabled = isMicrophoneEnabled;
}
public @NonNull State getState() {
@@ -68,6 +79,14 @@ public class WebRtcViewModel {
return localVideoEnabled;
}
+ public boolean isBluetoothAvailable() {
+ return isBluetoothAvailable;
+ }
+
+ public boolean isMicrophoneEnabled() {
+ return isMicrophoneEnabled;
+ }
+
public String toString() {
return "[State: " + state + ", recipient: " + recipient.getNumber() + ", identity: " + identityKey + ", remoteVideo: " + remoteVideoEnabled + ", localVideo: " + localVideoEnabled + "]";
}
diff --git a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
index 99142a4067..001f7acfe7 100644
--- a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
+++ b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
@@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.service;
import android.app.Service;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
@@ -23,10 +25,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.redphone.RedPhoneService;
-import org.thoughtcrime.redphone.audio.IncomingRinger;
import org.thoughtcrime.redphone.call.LockManager;
import org.thoughtcrime.redphone.pstn.IncomingPstnCallReceiver;
-import org.thoughtcrime.redphone.util.AudioUtils;
import org.thoughtcrime.redphone.util.UncaughtExceptionHandlerManager;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity;
@@ -52,7 +52,9 @@ import org.thoughtcrime.securesms.webrtc.WebRtcDataProtos;
import org.thoughtcrime.securesms.webrtc.WebRtcDataProtos.Connected;
import org.thoughtcrime.securesms.webrtc.WebRtcDataProtos.Data;
import org.thoughtcrime.securesms.webrtc.WebRtcDataProtos.Hangup;
+import org.thoughtcrime.securesms.webrtc.audio.BluetoothStateManager;
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
+import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.webrtc.AudioTrack;
import org.webrtc.DataChannel;
import org.webrtc.EglBase;
@@ -97,12 +99,11 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
-
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_RINGING;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_OUTGOING_RINGING;
-public class WebRtcCallService extends Service implements InjectableType, PeerConnection.Observer, DataChannel.Observer {
+public class WebRtcCallService extends Service implements InjectableType, PeerConnection.Observer, DataChannel.Observer, BluetoothStateManager.BluetoothStateListener {
private static final String TAG = WebRtcCallService.class.getSimpleName();
@@ -114,6 +115,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
public static final String EXTRA_REMOTE_NUMBER = "remote_number";
public static final String EXTRA_MUTE = "mute_value";
+ public static final String EXTRA_AVAILABLE = "enabled_value";
public static final String EXTRA_REMOTE_DESCRIPTION = "remote_description";
public static final String EXTRA_TIMESTAMP = "timestamp";
public static final String EXTRA_CALL_ID = "call_id";
@@ -122,15 +124,17 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
public static final String EXTRA_ICE_SDP_LINE_INDEX = "ice_sdp_line_index";
public static final String EXTRA_RESULT_RECEIVER = "result_receiver";
- public static final String ACTION_INCOMING_CALL = "CALL_INCOMING";
- public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING";
- public static final String ACTION_ANSWER_CALL = "ANSWER_CALL";
- public static final String ACTION_DENY_CALL = "DENY_CALL";
- public static final String ACTION_LOCAL_HANGUP = "LOCAL_HANGUP";
- public static final String ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO";
- public static final String ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO";
- public static final String ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT";
- public static final String ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL";
+ public static final String ACTION_INCOMING_CALL = "CALL_INCOMING";
+ public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING";
+ public static final String ACTION_ANSWER_CALL = "ANSWER_CALL";
+ public static final String ACTION_DENY_CALL = "DENY_CALL";
+ public static final String ACTION_LOCAL_HANGUP = "LOCAL_HANGUP";
+ public static final String ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO";
+ public static final String ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO";
+ public static final String ACTION_BLUETOOTH_CHANGE = "BLUETOOTH_CHANGE";
+ public static final String ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE";
+ public static final String ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT";
+ public static final String ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL";
public static final String ACTION_RESPONSE_MESSAGE = "RESPONSE_MESSAGE";
public static final String ACTION_ICE_MESSAGE = "ICE_MESSAGE";
@@ -142,9 +146,10 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
public static final String ACTION_ICE_CONNECTED = "ICE_CONNECTED";
private CallState callState = CallState.STATE_IDLE;
- private boolean audioEnabled = true;
+ private boolean microphoneEnabled = true;
private boolean localVideoEnabled = false;
private boolean remoteVideoEnabled = false;
+ private boolean bluetoothAvailable = false;
private Handler serviceHandler = new Handler();
@Inject public SignalMessageSenderFactory messageSenderFactory;
@@ -152,8 +157,9 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
private SignalServiceMessageSender messageSender;
private PeerConnectionFactory peerConnectionFactory;
- private IncomingRinger incomingRinger;
- private OutgoingRinger outgoingRinger;
+ private SignalAudioManager audioManager;
+ private BluetoothStateManager bluetoothStateManager;
+ private WiredHeadsetStateReceiver wiredHeadsetStateReceiver;
private LockManager lockManager;
private IncomingPstnCallReceiver callReceiver;
@@ -178,10 +184,10 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
super.onCreate();
initializeResources();
- initializeRingers();
registerIncomingPstnCallReceiver();
registerUncaughtExceptionHandler();
+ registerWiredHeadsetStateReceiver();
}
@Override
@@ -202,6 +208,8 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
else if (intent.getAction().equals(ACTION_REMOTE_HANGUP)) handleRemoteHangup(intent);
else if (intent.getAction().equals(ACTION_SET_MUTE_AUDIO)) handleSetMuteAudio(intent);
else if (intent.getAction().equals(ACTION_SET_MUTE_VIDEO)) handleSetMuteVideo(intent);
+ else if (intent.getAction().equals(ACTION_BLUETOOTH_CHANGE)) handleBluetoothChange(intent);
+ else if (intent.getAction().equals(ACTION_WIRED_HEADSET_CHANGE)) handleWiredHeadsetChange(intent);
else if (intent.getAction().equals(ACTION_REMOTE_VIDEO_MUTE)) handleRemoteVideoMute(intent);
else if (intent.getAction().equals(ACTION_RESPONSE_MESSAGE)) handleResponseMessage(intent);
else if (intent.getAction().equals(ACTION_ICE_MESSAGE)) handleRemoteIceCandidate(intent);
@@ -227,21 +235,37 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
if (uncaughtExceptionHandlerManager != null) {
uncaughtExceptionHandlerManager.unregister();
}
+
+ if (bluetoothStateManager != null) {
+ bluetoothStateManager.onDestroy();
+ }
+
+ if (wiredHeadsetStateReceiver != null) {
+ unregisterReceiver(wiredHeadsetStateReceiver);
+ }
+ }
+
+ @Override
+ public void onBluetoothStateChanged(boolean isAvailable) {
+ Log.w(TAG, "onBluetoothStateChanged: " + isAvailable);
+
+ Intent intent = new Intent(this, WebRtcCallService.class);
+ intent.setAction(ACTION_BLUETOOTH_CHANGE);
+ intent.putExtra(EXTRA_AVAILABLE, isAvailable);
+
+ startService(intent);
}
// Initializers
- private void initializeRingers() {
- this.outgoingRinger = new OutgoingRinger(this);
- this.incomingRinger = new IncomingRinger(this);
- }
-
private void initializeResources() {
ApplicationContext.getInstance(this).injectDependencies(this);
this.callState = CallState.STATE_IDLE;
this.lockManager = new LockManager(this);
this.peerConnectionFactory = new PeerConnectionFactory(new PeerConnectionFactoryOptions());
+ this.audioManager = new SignalAudioManager(this);
+ this.bluetoothStateManager = new BluetoothStateManager(this, this);
this.messageSender = messageSenderFactory.create();
this.messageSender.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10));
this.accountManager.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10));
@@ -257,6 +281,20 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
uncaughtExceptionHandlerManager.registerHandler(new ProximityLockRelease(lockManager));
}
+ 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));
+ }
+
// Handlers
private void handleIncomingCall(final Intent intent) {
@@ -278,6 +316,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
timeoutExecutor.schedule(new TimeoutRunnable(this.callId), 2, TimeUnit.MINUTES);
initializeVideo();
+
retrieveTurnServers().addListener(new SuccessOnlyListener>(this.callState, this.callId) {
@Override
public void onSuccessContinue(List result) {
@@ -323,9 +362,11 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
initializeVideo();
- sendMessage(WebRtcViewModel.State.CALL_OUTGOING, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_OUTGOING, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL);
- outgoingRinger.playSonar();
+ audioManager.initializeAudioForCall();
+ audioManager.startOutgoingRinger(OutgoingRinger.Type.SONAR);
+ bluetoothStateManager.setWantsConnection(true);
setCallInProgressNotification(TYPE_OUTGOING_RINGING, recipient);
DatabaseFactory.getSmsDatabase(this).insertOutgoingCall(recipient.getNumber());
@@ -355,11 +396,11 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
Log.w(TAG, error);
if (error instanceof UntrustedIdentityException) {
- sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, recipient, ((UntrustedIdentityException)error).getIdentityKey(), localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, recipient, ((UntrustedIdentityException)error).getIdentityKey(), localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
} else if (error instanceof UnregisteredUserException) {
- sendMessage(WebRtcViewModel.State.NO_SUCH_USER, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.NO_SUCH_USER, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
} else if (error instanceof IOException) {
- sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
}
terminate();
@@ -396,7 +437,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
@Override
public void onFailureContinue(Throwable error) {
Log.w(TAG, error);
- sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
terminate();
}
@@ -445,7 +486,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
@Override
public void onFailureContinue(Throwable error) {
Log.w(TAG, error);
- sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
terminate();
}
@@ -459,17 +500,19 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
this.callState = CallState.STATE_LOCAL_RINGING;
this.lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
- sendMessage(WebRtcViewModel.State.CALL_INCOMING, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_INCOMING, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
startCallCardActivity();
- incomingRinger.start();
+ audioManager.initializeAudioForCall();
+ audioManager.startIncomingRinger();
+
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient);
} else if (callState == CallState.STATE_DIALING) {
if (this.recipient == null) throw new AssertionError("assert");
this.callState = CallState.STATE_REMOTE_RINGING;
- this.outgoingRinger.playRing();
+ this.audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING);
- sendMessage(WebRtcViewModel.State.CALL_RINGING, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_RINGING, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
}
}
@@ -488,19 +531,19 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
throw new AssertionError("assert");
}
- callState = CallState.STATE_CONNECTED;
+ audioManager.startCommunication(callState == CallState.STATE_REMOTE_RINGING);
+ bluetoothStateManager.setWantsConnection(true);
- initializeAudio();
- outgoingRinger.playComplete();
+ callState = CallState.STATE_CONNECTED;
if (localVideoEnabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO);
else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL);
- sendMessage(WebRtcViewModel.State.CALL_CONNECTED, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_CONNECTED, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
setCallInProgressNotification(TYPE_ESTABLISHED, recipient);
- this.peerConnection.setAudioEnabled(audioEnabled);
+ this.peerConnection.setAudioEnabled(microphoneEnabled);
this.peerConnection.setVideoEnabled(localVideoEnabled);
this.dataChannel.send(new DataChannel.Buffer(ByteBuffer.wrap(Data.newBuilder()
@@ -529,9 +572,9 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return;
}
- sendMessage(WebRtcViewModel.State.CALL_BUSY, recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_BUSY, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
- outgoingRinger.playBusy();
+ audioManager.startOutgoingRinger(OutgoingRinger.Type.BUSY);
serviceHandler.postDelayed(new Runnable() {
@Override
public void run() {
@@ -546,7 +589,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
this.callState != CallState.STATE_CONNECTED)
{
Log.w(TAG, "Timing out call: " + this.callId);
- sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
terminate();
}
}
@@ -575,8 +618,6 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
throw new AssertionError("assert");
}
- incomingRinger.stop();
-
DatabaseFactory.getSmsDatabase(this).insertReceivedCall(recipient.getNumber());
this.peerConnection.setAudioEnabled(true);
@@ -613,7 +654,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
this.dataChannel.send(new DataChannel.Buffer(ByteBuffer.wrap(Data.newBuilder().setHangup(Hangup.newBuilder().setId(this.callId)).build().toByteArray()), false));
sendMessage(this.recipient, SignalServiceCallMessage.forHangup(new HangupMessage(this.callId)));
- sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
}
terminate();
@@ -630,9 +671,9 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
}
if (this.callState == CallState.STATE_DIALING || this.callState == CallState.STATE_REMOTE_RINGING) {
- sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, this.recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
} else {
- sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
}
if (this.callState == CallState.STATE_ANSWERING || this.callState == CallState.STATE_LOCAL_RINGING) {
@@ -644,15 +685,17 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
private void handleSetMuteAudio(Intent intent) {
boolean muted = intent.getBooleanExtra(EXTRA_MUTE, false);
- this.audioEnabled = !muted;
+ this.microphoneEnabled = !muted;
if (this.peerConnection != null) {
- this.peerConnection.setAudioEnabled(this.audioEnabled);
+ this.peerConnection.setAudioEnabled(this.microphoneEnabled);
}
}
private void handleSetMuteVideo(Intent intent) {
- boolean muted = intent.getBooleanExtra(EXTRA_MUTE, false);
+ AudioManager audioManager = ServiceUtil.getAudioManager(this);
+ boolean muted = intent.getBooleanExtra(EXTRA_MUTE, false);
+
this.localVideoEnabled = !muted;
if (this.peerConnection != null) {
@@ -672,7 +715,42 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
else this.lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL);
}
- sendMessage(viewModelStateFor(callState), this.recipient, localVideoEnabled, remoteVideoEnabled);
+ if (localVideoEnabled && !audioManager.isSpeakerphoneOn() && !audioManager.isBluetoothScoOn()) {
+ audioManager.setSpeakerphoneOn(true);
+ }
+
+ sendMessage(viewModelStateFor(callState), this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
+ }
+
+ private void handleBluetoothChange(Intent intent) {
+ this.bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false);
+
+ if (recipient != null) {
+ sendMessage(viewModelStateFor(callState), recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
+ }
+ }
+
+ private void handleWiredHeadsetChange(Intent intent) {
+ Log.w(TAG, "handleWiredHeadsetChange...");
+
+ if (callState == CallState.STATE_CONNECTED ||
+ callState == CallState.STATE_DIALING ||
+ callState == CallState.STATE_REMOTE_RINGING)
+ {
+ AudioManager audioManager = ServiceUtil.getAudioManager(this);
+ boolean present = intent.getBooleanExtra(EXTRA_AVAILABLE, false);
+
+ if (present && audioManager.isSpeakerphoneOn()) {
+ audioManager.setSpeakerphoneOn(false);
+ audioManager.setBluetoothScoOn(false);
+ } else if (!present && !audioManager.isSpeakerphoneOn() && !audioManager.isBluetoothScoOn() && localVideoEnabled) {
+ audioManager.setSpeakerphoneOn(true);
+ }
+
+ if (recipient != null) {
+ sendMessage(viewModelStateFor(callState), recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
+ }
+ }
}
private void handleRemoteVideoMute(Intent intent) {
@@ -685,7 +763,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
}
this.remoteVideoEnabled = !muted;
- sendMessage(WebRtcViewModel.State.CALL_CONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled);
+ sendMessage(WebRtcViewModel.State.CALL_CONNECTED, this.recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
}
/// Helper Methods
@@ -706,15 +784,6 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return System.currentTimeMillis() - intent.getLongExtra(WebRtcCallService.EXTRA_TIMESTAMP, -1) > TimeUnit.MINUTES.toMillis(2);
}
- private void initializeAudio() {
- AudioManager audioManager = ServiceUtil.getAudioManager(this);
- AudioUtils.resetConfiguration(this);
-
- Log.d(TAG, "request STREAM_VOICE_CALL transient audio focus");
- audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL,
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
- }
-
private void initializeVideo() {
Util.runOnMainSync(new Runnable() {
@Override
@@ -737,22 +806,12 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient));
}
- private void shutdownAudio() {
- Log.d(TAG, "reset audio mode and abandon focus");
- AudioUtils.resetConfiguration(this);
- AudioManager am = ServiceUtil.getAudioManager(this);
- am.setMode(AudioManager.MODE_NORMAL);
- am.abandonAudioFocus(null);
- am.stopBluetoothSco();
- }
-
private synchronized void terminate() {
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING);
stopForeground(true);
- incomingRinger.stop();
- outgoingRinger.stop();
- outgoingRinger.playDisconnected();
+ audioManager.stop(callState == CallState.STATE_DIALING || callState == CallState.STATE_REMOTE_RINGING || callState == CallState.STATE_CONNECTED);
+ bluetoothStateManager.setWantsConnection(false);
if (peerConnection != null) {
peerConnection.dispose();
@@ -769,12 +828,10 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
eglBase = null;
}
- shutdownAudio();
-
this.callState = CallState.STATE_IDLE;
this.recipient = null;
this.callId = null;
- this.audioEnabled = true;
+ this.microphoneEnabled = true;
this.localVideoEnabled = false;
this.remoteVideoEnabled = false;
this.pendingIceUpdates = null;
@@ -784,17 +841,19 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
private void sendMessage(@NonNull WebRtcViewModel.State state,
@NonNull Recipient recipient,
- boolean localVideoEnabled, boolean remoteVideoEnabled)
+ boolean localVideoEnabled, boolean remoteVideoEnabled,
+ boolean bluetoothAvailable, boolean microphoneEnabled)
{
- EventBus.getDefault().postSticky(new WebRtcViewModel(state, recipient, localVideoEnabled, remoteVideoEnabled));
+ EventBus.getDefault().postSticky(new WebRtcViewModel(state, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled));
}
private void sendMessage(@NonNull WebRtcViewModel.State state,
@NonNull Recipient recipient,
@NonNull IdentityKey identityKey,
- boolean localVideoEnabled, boolean remoteVideoEnabled)
+ boolean localVideoEnabled, boolean remoteVideoEnabled,
+ boolean bluetoothAvailable, boolean microphoneEnabled)
{
- EventBus.getDefault().postSticky(new WebRtcViewModel(state, recipient, identityKey, localVideoEnabled, remoteVideoEnabled));
+ EventBus.getDefault().postSticky(new WebRtcViewModel(state, recipient, identityKey, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled));
}
private ListenableFutureTask sendMessage(@NonNull final Recipient recipient,
@@ -1022,6 +1081,8 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return futureTask;
}
+ ////
+
private WebRtcViewModel.State viewModelStateFor(CallState state) {
switch (state) {
case STATE_CONNECTED: return WebRtcViewModel.State.CALL_CONNECTED;
@@ -1035,6 +1096,20 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return WebRtcViewModel.State.CALL_DISCONNECTED;
}
+ ///
+
+ private static class WiredHeadsetStateReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int state = intent.getIntExtra("state", -1);
+
+ Intent serviceIntent = new Intent(context, WebRtcCallService.class);
+ serviceIntent.setAction(WebRtcCallService.ACTION_WIRED_HEADSET_CHANGE);
+ serviceIntent.putExtra(WebRtcCallService.EXTRA_AVAILABLE, state != 0);
+ context.startService(serviceIntent);
+ }
+ }
+
private class TimeoutRunnable implements Runnable {
private final long callId;
diff --git a/src/org/thoughtcrime/securesms/webrtc/audio/BluetoothStateManager.java b/src/org/thoughtcrime/securesms/webrtc/audio/BluetoothStateManager.java
new file mode 100644
index 0000000000..704a9e9da9
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/webrtc/audio/BluetoothStateManager.java
@@ -0,0 +1,227 @@
+package org.thoughtcrime.securesms.webrtc.audio;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+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 android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.util.ServiceUtil;
+
+import java.util.List;
+
+public class BluetoothStateManager {
+
+ private static final String TAG = BluetoothStateManager.class.getSimpleName();
+
+ private enum ScoConnection {
+ DISCONNECTED,
+ IN_PROGRESS,
+ CONNECTED
+ }
+
+ private final Object LOCK = new Object();
+
+ private final Context context;
+ private final BluetoothAdapter bluetoothAdapter;
+ private final BluetoothScoReceiver bluetoothScoReceiver;
+ private final BluetoothConnectionReceiver bluetoothConnectionReceiver;
+ private final BluetoothStateListener listener;
+
+ private BluetoothHeadset bluetoothHeadset = null;
+ private ScoConnection scoConnection = ScoConnection.DISCONNECTED;
+ private boolean wantsConnection = false;
+
+ public BluetoothStateManager(@NonNull Context context, @Nullable BluetoothStateListener listener) {
+ this.context = context.getApplicationContext();
+ this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ this.bluetoothScoReceiver = new BluetoothScoReceiver();
+ this.bluetoothConnectionReceiver = new BluetoothConnectionReceiver();
+ this.listener = listener;
+
+ requestHeadsetProxyProfile();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ context.registerReceiver(bluetoothConnectionReceiver, new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED));
+ }
+
+ Intent sticky = context.registerReceiver(bluetoothScoReceiver, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED));
+
+ if (sticky != null) {
+ bluetoothScoReceiver.onReceive(context, sticky);
+ }
+
+ handleBluetoothStateChange();
+ }
+
+ public void onDestroy() {
+ if (bluetoothHeadset != null && bluetoothAdapter != null && Build.VERSION.SDK_INT >= 11) {
+ this.bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
+ }
+
+ if (Build.VERSION.SDK_INT >= 11 && bluetoothConnectionReceiver != null) {
+ context.unregisterReceiver(bluetoothConnectionReceiver);
+ }
+
+ if (bluetoothScoReceiver != null) {
+ context.unregisterReceiver(bluetoothScoReceiver);
+ }
+
+ this.bluetoothHeadset = null;
+ }
+
+ public void setWantsConnection(boolean enabled) {
+ synchronized (LOCK) {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+
+ this.wantsConnection = enabled;
+
+ if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
+ 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 (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;
+
+ if (Build.VERSION.SDK_INT >= 11) {
+ return bluetoothHeadset != null && !bluetoothHeadset.getConnectedDevices().isEmpty();
+ } else {
+ return audioManager.isBluetoothScoOn() || audioManager.isBluetoothA2dpOn();
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, e);
+ return false;
+ }
+ }
+
+ private String getScoChangeIntent() {
+ if (Build.VERSION.SDK_INT >= 14) {
+ return AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED;
+ } else {
+ return AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED;
+ }
+ }
+
+
+ private void requestHeadsetProxyProfile() {
+ if (Build.VERSION.SDK_INT >= 11) {
+ this.bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ 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.w(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.w(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 (Build.VERSION.SDK_INT >= 11 && bluetoothHeadset != null) {
+ List devices = bluetoothHeadset.getConnectedDevices();
+
+ for (BluetoothDevice device : devices) {
+ if (bluetoothHeadset.isAudioConnected(device)) {
+ int deviceClass = device.getBluetoothClass().getDeviceClass();
+
+ if (deviceClass == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
+ deviceClass == BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO ||
+ deviceClass == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)
+ {
+ scoConnection = ScoConnection.CONNECTED;
+
+ if (wantsConnection) {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+ audioManager.setBluetoothScoOn(true);
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+ }
+
+ handleBluetoothStateChange();
+ }
+ }
+
+ private class BluetoothConnectionReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.w(TAG, "onReceive");
+ handleBluetoothStateChange();
+ }
+ }
+
+ public interface BluetoothStateListener {
+ public void onBluetoothStateChanged(boolean isAvailable);
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java b/src/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java
new file mode 100644
index 0000000000..48eca28085
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java
@@ -0,0 +1,142 @@
+package org.thoughtcrime.securesms.webrtc.audio;
+
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.util.ServiceUtil;
+
+import java.io.IOException;
+
+public class IncomingRinger {
+
+ private static final String TAG = IncomingRinger.class.getSimpleName();
+
+ private static final long[] VIBRATE_PATTERN = {0, 1000, 1000};
+
+ private final Context context;
+ private final Vibrator vibrator;
+
+ private MediaPlayer player;
+
+ public IncomingRinger(Context context) {
+ this.context = context.getApplicationContext();
+ this.vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ }
+
+ public void start(boolean speakerphone) {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+
+ if (player != null) player.release();
+ player = createPlayer();
+
+ int ringerMode = audioManager.getRingerMode();
+
+ if (shouldVibrate(context, player, ringerMode)) {
+ Log.i(TAG, "Starting vibration");
+ vibrator.vibrate(VIBRATE_PATTERN, 1);
+ }
+
+ if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
+ try {
+ if (!player.isPlaying()) {
+ player.prepare();
+ player.start();
+ Log.w(TAG, "Playing ringtone now...");
+ } else {
+ Log.w(TAG, "Ringtone is already playing, declining to restart.");
+ }
+ } catch (IllegalStateException | IOException e) {
+ Log.w(TAG, e);
+ player = null;
+ }
+ } else {
+ Log.w(TAG, "Not ringing, mode: " + ringerMode);
+ }
+
+ if (speakerphone) {
+ audioManager.setSpeakerphoneOn(true);
+ }
+ }
+
+ public void stop() {
+ if (player != null) {
+ Log.w(TAG, "Stopping ringer");
+ player.release();
+ player = null;
+ }
+
+ Log.w(TAG, "Cancelling vibrator");
+ vibrator.cancel();
+ }
+
+ private boolean shouldVibrate(Context context, MediaPlayer player, int ringerMode) {
+ if (player == null) {
+ return true;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ return shouldVibrateNew(context, ringerMode);
+ } else {
+ return shouldVibrateOld(context);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ private boolean shouldVibrateNew(Context context, int ringerMode) {
+ Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+
+ if (vibrator == null || !vibrator.hasVibrator()) {
+ return false;
+ }
+
+ boolean vibrateWhenRinging = Settings.System.getInt(context.getContentResolver(), "vibrate_when_ringing", 0) != 0;
+
+ if (vibrateWhenRinging) {
+ return ringerMode != AudioManager.RINGER_MODE_SILENT;
+ } else {
+ return ringerMode == AudioManager.RINGER_MODE_VIBRATE;
+ }
+ }
+
+ private boolean shouldVibrateOld(Context context) {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+ return audioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER);
+ }
+
+ private MediaPlayer createPlayer() {
+ try {
+ MediaPlayer mediaPlayer = new MediaPlayer();
+ Uri ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
+
+ mediaPlayer.setOnErrorListener(new MediaPlayerErrorListener());
+ mediaPlayer.setDataSource(context, ringtoneUri);
+ mediaPlayer.setLooping(true);
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
+
+ return mediaPlayer;
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to create player for incoming call ringer");
+ return null;
+ }
+ }
+
+
+ private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener {
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ Log.w(TAG, "onError(" + mp + ", " + what + ", " + extra);
+ player = null;
+ return false;
+ }
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/webrtc/audio/OutgoingRinger.java b/src/org/thoughtcrime/securesms/webrtc/audio/OutgoingRinger.java
index 1fda23ec9b..a61d090928 100644
--- a/src/org/thoughtcrime/securesms/webrtc/audio/OutgoingRinger.java
+++ b/src/org/thoughtcrime/securesms/webrtc/audio/OutgoingRinger.java
@@ -4,84 +4,56 @@ import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
+import android.support.annotation.NonNull;
import android.util.Log;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.util.ServiceUtil;
import java.io.IOException;
-/**
- * Handles loading and playing the sequence of sounds we use to indicate call initialization.
- *
- * @author Stuart O. Anderson
- */
-public class OutgoingRinger implements MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener {
+public class OutgoingRinger {
private static final String TAG = OutgoingRinger.class.getSimpleName();
+ public enum Type {
+ SONAR,
+ RINGING,
+ BUSY
+ }
+
+ private final Context context;
+
private MediaPlayer mediaPlayer;
- private int currentSoundID;
- private boolean loopEnabled;
- private Context context;
-
- public OutgoingRinger(Context context) {
- this.context = context;
-
- loopEnabled = true;
- currentSoundID = -1;
+ public OutgoingRinger(@NonNull Context context) {
+ this.context = context;
}
+
+ public void start(Type type) {
+ int soundId;
- public void playSonar() {
- start(R.raw.redphone_sonarping);
- }
+ if (type == Type.SONAR) soundId = R.raw.redphone_sonarping;
+ else if (type == Type.RINGING) soundId = R.raw.redphone_outring;
+ else if (type == Type.BUSY) soundId = R.raw.redphone_busy;
+ else throw new IllegalArgumentException("Not a valid sound type");
- public void playRing() {
- start(R.raw.redphone_outring);
- }
+ if( mediaPlayer != null ) {
+ mediaPlayer.release();
+ }
- public void playComplete() {
- stop(R.raw.webrtc_completed);
- }
-
- public void playDisconnected() {
- stop(R.raw.webrtc_disconnected);
- }
-
- public void playBusy() {
- start(R.raw.redphone_busy);
- }
-
- private void setSound( int soundID ) {
- currentSoundID = soundID;
- loopEnabled = true;
- }
-
- private void start( int soundID ) {
- if( soundID == currentSoundID ) return;
- setSound( soundID );
- start();
- }
-
- private void start() {
- if( mediaPlayer != null ) mediaPlayer.release();
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
- mediaPlayer.setOnCompletionListener(this);
- mediaPlayer.setOnPreparedListener(this);
- mediaPlayer.setLooping(loopEnabled);
+ mediaPlayer.setLooping(true);
String packageName = context.getPackageName();
- Uri dataUri = Uri.parse("android.resource://" + packageName + "/" + currentSoundID);
+ Uri dataUri = Uri.parse("android.resource://" + packageName + "/" + soundId);
try {
mediaPlayer.setDataSource(context, dataUri);
- mediaPlayer.prepareAsync();
+ mediaPlayer.prepare();
+ mediaPlayer.start();
} catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) {
Log.w(TAG, e);
- // TODO Auto-generated catch block
- return;
}
}
@@ -89,37 +61,5 @@ public class OutgoingRinger implements MediaPlayer.OnCompletionListener, MediaPl
if (mediaPlayer == null) return;
mediaPlayer.release();
mediaPlayer = null;
-
- currentSoundID = -1;
- }
-
- private void stop( int soundID ) {
- setSound( soundID );
- loopEnabled = false;
- start();
- }
-
- public void onCompletion(MediaPlayer mp) {
- //mediaPlayer.release();
- //mediaPlayer = null;
- }
-
- public void onPrepared(MediaPlayer mp) {
- AudioManager am = ServiceUtil.getAudioManager(context);
-
- if (am.isBluetoothScoAvailableOffCall()) {
- Log.d(TAG, "bluetooth sco is available");
- try {
- am.startBluetoothSco();
- } catch (NullPointerException e) {
- // Lollipop bug (https://stackoverflow.com/questions/26642218/audiomanager-startbluetoothsco-crashes-on-android-lollipop)
- }
- }
-
- try {
- mp.start();
- } catch (IllegalStateException e) {
- Log.w(TAG, e);
- }
}
}
diff --git a/src/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.java b/src/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.java
new file mode 100644
index 0000000000..46ef6f1860
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.java
@@ -0,0 +1,111 @@
+package org.thoughtcrime.securesms.webrtc.audio;
+
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.SoundPool;
+import android.os.Build;
+import android.support.annotation.NonNull;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.ServiceUtil;
+
+public class SignalAudioManager {
+
+ private static final String TAG = SignalAudioManager.class.getSimpleName();
+
+ private final Context context;
+ private final IncomingRinger incomingRinger;
+ private final OutgoingRinger outgoingRinger;
+
+ private final SoundPool soundPool;
+ private final int connectedSoundId;
+ private final int disconnectedSoundId;
+
+ public SignalAudioManager(@NonNull Context context) {
+ this.context = context.getApplicationContext();
+ this.incomingRinger = new IncomingRinger(context);
+ this.outgoingRinger = new OutgoingRinger(context);
+ this.soundPool = new SoundPool(1, AudioManager.STREAM_VOICE_CALL, 0);
+
+ 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() {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE);
+ } else {
+ audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ }
+ }
+
+ public void startIncomingRinger() {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+ boolean speaker = !audioManager.isWiredHeadsetOn() && !audioManager.isBluetoothScoOn();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ } else {
+ audioManager.setMode(AudioManager.MODE_IN_CALL);
+ }
+
+ audioManager.setMicrophoneMute(false);
+ audioManager.setSpeakerphoneOn(speaker);
+
+ incomingRinger.start(speaker);
+ }
+
+ public void startOutgoingRinger(OutgoingRinger.Type type) {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+ audioManager.setMicrophoneMute(false);
+
+ if (type == OutgoingRinger.Type.SONAR) {
+ audioManager.setSpeakerphoneOn(false);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ } else {
+ audioManager.setMode(AudioManager.MODE_IN_CALL);
+ }
+
+ outgoingRinger.start(type);
+ }
+
+ public void startCommunication(boolean preserveSpeakerphone) {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+
+ incomingRinger.stop();
+ outgoingRinger.stop();
+
+ if (!preserveSpeakerphone) {
+ audioManager.setSpeakerphoneOn(false);
+ }
+
+ soundPool.play(connectedSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
+ }
+
+ public void stop(boolean playDisconnected) {
+ AudioManager audioManager = ServiceUtil.getAudioManager(context);
+
+ incomingRinger.stop();
+ outgoingRinger.stop();
+
+ if (playDisconnected) {
+ soundPool.play(disconnectedSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
+ }
+
+ if (audioManager.isBluetoothScoOn()) {
+ audioManager.setBluetoothScoOn(false);
+ audioManager.stopBluetoothSco();
+ }
+
+ audioManager.setSpeakerphoneOn(false);
+ audioManager.setMicrophoneMute(false);
+ audioManager.setMode(AudioManager.MODE_NORMAL);
+ audioManager.abandonAudioFocus(null);
+ }
+}