From cd28cd172f770eeae08f7adca8964284354ee479 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sat, 4 Mar 2017 15:48:10 -0800 Subject: [PATCH] Refactor webrtc audio management Attempts to: 1) Successfully play ringtone through speaker instead of earpiece when possible. 2) Manage bluetooth headset connectivity as well as possible 3) Eliminate notification sounds while in-call when possible 4) Make sure audio is correctly setup when receiving calls Fixes #6271 Fixes #6248 Fixes #6238 Fixes #6184 Fixes #6169 // FREEBIE --- res/drawable-hdpi/ic_bluetooth_white_24dp.png | Bin 0 -> 307 bytes res/drawable-mdpi/ic_bluetooth_white_24dp.png | Bin 0 -> 213 bytes .../ic_bluetooth_white_24dp.png | Bin 0 -> 344 bytes .../ic_bluetooth_white_24dp.png | Bin 0 -> 502 bytes .../ic_bluetooth_white_24dp.png | Bin 0 -> 595 bytes res/drawable/webrtc_audio_button.xml | 47 ---- res/drawable/webrtc_bluetooth_button.xml | 9 + res/drawable/webrtc_speaker_button.xml | 9 + res/layout/webrtc_call_controls.xml | 10 +- res/layout/webrtc_call_screen.xml | 1 - .../securesms/WebRtcCallActivity.java | 101 ++------ .../components/webrtc/WebRtcCallControls.java | 132 ++++++---- .../components/webrtc/WebRtcCallScreen.java | 17 +- .../webrtc/WebRtcInCallAudioButton.java | 189 -------------- .../securesms/events/WebRtcViewModel.java | 35 ++- .../securesms/service/WebRtcCallService.java | 233 ++++++++++++------ .../webrtc/audio/BluetoothStateManager.java | 227 +++++++++++++++++ .../webrtc/audio/IncomingRinger.java | 142 +++++++++++ .../webrtc/audio/OutgoingRinger.java | 112 ++------- .../webrtc/audio/SignalAudioManager.java | 111 +++++++++ 20 files changed, 841 insertions(+), 534 deletions(-) create mode 100644 res/drawable-hdpi/ic_bluetooth_white_24dp.png create mode 100644 res/drawable-mdpi/ic_bluetooth_white_24dp.png create mode 100644 res/drawable-xhdpi/ic_bluetooth_white_24dp.png create mode 100644 res/drawable-xxhdpi/ic_bluetooth_white_24dp.png create mode 100644 res/drawable-xxxhdpi/ic_bluetooth_white_24dp.png delete mode 100644 res/drawable/webrtc_audio_button.xml create mode 100644 res/drawable/webrtc_bluetooth_button.xml create mode 100644 res/drawable/webrtc_speaker_button.xml delete mode 100644 src/org/thoughtcrime/securesms/components/webrtc/WebRtcInCallAudioButton.java create mode 100644 src/org/thoughtcrime/securesms/webrtc/audio/BluetoothStateManager.java create mode 100644 src/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java create mode 100644 src/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.java 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 0000000000000000000000000000000000000000..fce1884000fa1d0de6e62c4636eeef661db5697d GIT binary patch literal 307 zcmV-30nGl1P)p?u0f@f35ayHFg(3nPuC6#9y!m@@)cN})aPQ4|%eZfJj~->mV(A(iT= z-1IZ!Lsu4|RA(vkM-v8}?cHRAQaxJ$c82MK8KrvNcmd$kk6apMM?51U;zR}iO5{=L zOfl%s(F#Qr;u75ujfBwC7OD|vCPN#W?xQ^b7HWa?q@~C-L z@Bzhz8X?V0iaQ!UPYPNb))dri(X(?+0;`-8$0@n(T<@omw=~eoFln_de7rM3KbJV%29x(;Ns%pb9sgNo>)wCwP7#=^{=6abF*ACdK{diJ zZ}!Z2l05ST;h!&nDKhU*6nS5wn)f4AeoHlUOD8=6WsEVfCra+wgx?U5%%@jP07Bz2m|lGet3Q7A6s|OWuK0%`5)QXyaAp> qB7um!0%GzBcovBS;_}xKG(G|4td3Rk)`IT<0000+OG&u~3Ps{fRIY>I3qe+b#rz`DYQmx8KlJq%)q$&%CCFSFo1F>6X>;&nT{843Z z>@KA9FE)xJ+jgA9k!#EnoXV>(O;VKOHfUiBx5Gk}6SS})X)i+JIFTklB)v^e;yKaB zps-(>mNdj+9u!L)ycGwWt8f_5CBD5iw@RGomaQ{#B8OUzLxh)fx8s%lI+%M*$05R% zGt#S)BfXUV^941VWH9iU18#0h{n0T3qu;sijP0EiO+aRMMt0K^FZDFHKVu=+Eg6ah1Q0_frd zfRJ#=M+yO;BOK)?5C*X?g$ccU0N8~HgVaf30yf5v2MGO-I#-y06?QpVdy2=U>_Kbx?{f@uvRm%x>OAnY^p zmy9bxsf3O)KbgRrpG4r!j}kgG3ag?%f=0ePK>RP1uoE}R`P_tz6Ir92&v`b;%I70w z=W`Lx%I6_?@;l@1m+`XNo4*=tyCx*~^UY;8P4}EEiOn1vHZZ| z?$X%7$0o${gUjos2`TxJ2OLd7LR$WrkC<*~@*@xVUd{YFqWr*4JeA1z9!95llgRg+ z$jaqAhv%Gp^HQQ@zH_+3M|!^Ty3G{tX8!$nr>|2&?h8+)^LvELymB^f{(*40m;T1j zckY6(U6#qufNJMQ01pa_yxREzfJVL-ppx$a=;WIKrF;YM=$tY@Ex!lQ%Rd1q<{tqx h^Xt^9Q>Tu|`~Zov&YaUBjBWq`002ovPDHLkV1l^i7E%BJ literal 0 HcmV?d00001 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); + } +}