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
This commit is contained in:
Moxie Marlinspike 2017-03-04 15:48:10 -08:00
parent 3904c76261
commit cd28cd172f
20 changed files with 841 additions and 534 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

View File

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/compoundBackgroundItem" android:drawable="@drawable/webrtc_control_background"/>
<item android:id="@+id/moreIndicatorItem"
android:top="5dp"
android:left="5dp"
android:right="5dp"
android:bottom="5dp">
<bitmap android:src="@drawable/redphone_ic_more_indicator_holo_dark"
android:gravity="bottom|right" />
</item>
<item android:id="@+id/bluetoothItem"
android:top="5dp"
android:left="5dp"
android:right="5dp"
android:bottom="5dp">
<bitmap android:src="@drawable/ic_phone_bluetooth_speaker_white_24dp"
android:gravity="center" />
</item>
<!-- Handset earpiece is active -->
<item android:id="@+id/handsetItem" android:top="5dp"
android:left="5dp"
android:right="5dp"
android:bottom="5dp">
<bitmap android:src="@drawable/ic_phone_in_talk_white_24dp"
android:gravity="center" />
</item>
<!-- Speakerphone icon showing 'speaker on' state -->
<item android:id="@+id/speakerphoneOnItem" android:top="5dp"
android:left="5dp"
android:right="5dp"
android:bottom="5dp">
<bitmap android:src="@drawable/ic_volume_up_white_24dp"
android:gravity="center" />
</item>
<!--&lt;!&ndash; Speakerphone icon showing 'speaker off' state &ndash;&gt;-->
<!--<item android:id="@+id/speakerphoneOffItem">-->
<!--<bitmap android:src="@drawable/ic_volume_mute_white_24dp"-->
<!--android:gravity="center" />-->
<!--</item>-->
</layer-list>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/webrtc_control_background"/>
<item android:top="5dp"
android:left="5dp"
android:right="5dp"
android:bottom="5dp"
android:drawable="@drawable/ic_bluetooth_white_24dp"/>
</layer-list>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/webrtc_control_background"/>
<item android:top="5dp"
android:left="5dp"
android:right="5dp"
android:bottom="5dp"
android:drawable="@drawable/ic_volume_up_white_24dp"/>
</layer-list>

View File

@ -7,12 +7,18 @@
android:orientation="horizontal" android:orientation="horizontal"
tools:background="@color/textsecure_primary"> tools:background="@color/textsecure_primary">
<ToggleButton android:id="@+id/audioButton" <ToggleButton android:id="@+id/speakerButton"
style="@style/WebRtcCallCompoundButton" style="@style/WebRtcCallCompoundButton"
android:background="@drawable/webrtc_audio_button" android:background="@drawable/webrtc_speaker_button"
tools:checked="true" tools:checked="true"
android:layout_marginRight="15dp"/> android:layout_marginRight="15dp"/>
<ToggleButton android:id="@+id/bluetoothButton"
style="@style/WebRtcCallCompoundButton"
android:background="@drawable/webrtc_bluetooth_button"
tools:checked="true"
android:layout_marginRight="15dp"
android:visibility="gone"/>
<ToggleButton android:id="@+id/muteButton" <ToggleButton android:id="@+id/muteButton"
style="@style/WebRtcCallCompoundButton" style="@style/WebRtcCallCompoundButton"

View File

@ -176,7 +176,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/textsecure_primary" android:background="@color/textsecure_primary"
android:paddingLeft="24dp" android:paddingLeft="24dp"
android:paddingRight="24dp" android:paddingRight="24dp"
android:paddingTop="16dp" android:paddingTop="16dp"

View File

@ -18,12 +18,9 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
@ -38,7 +35,6 @@ import android.view.WindowManager;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.redphone.util.AudioUtils;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
import org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay; import org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay;
@ -66,8 +62,6 @@ public class WebRtcCallActivity extends Activity {
public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION"; public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION";
private WebRtcCallScreen callScreen; private WebRtcCallScreen callScreen;
private BroadcastReceiver bluetoothStateReceiver;
private BroadcastReceiver wiredHeadsetStateReceiver;
private SignalServiceNetworkAccess networkAccess; private SignalServiceNetworkAccess networkAccess;
@Override @Override
@ -93,9 +87,6 @@ public class WebRtcCallActivity extends Activity {
if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStarted(this); if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStarted(this);
initializeScreenshotSecurity(); initializeScreenshotSecurity();
EventBus.getDefault().register(this); EventBus.getDefault().register(this);
registerBluetoothReceiver();
registerWiredHeadsetReceiver();
} }
@Override @Override
@ -116,8 +107,6 @@ public class WebRtcCallActivity extends Activity {
super.onPause(); super.onPause();
if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStopped(this); if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStopped(this);
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
unregisterReceiver(bluetoothStateReceiver);
unregisterReceiver(wiredHeadsetStateReceiver);
} }
@Override @Override
@ -141,7 +130,8 @@ public class WebRtcCallActivity extends Activity {
callScreen.setIncomingCallActionListener(new IncomingCallActionListener()); callScreen.setIncomingCallActionListener(new IncomingCallActionListener());
callScreen.setAudioMuteButtonListener(new AudioMuteButtonListener()); callScreen.setAudioMuteButtonListener(new AudioMuteButtonListener());
callScreen.setVideoMuteButtonListener(new VideoMuteButtonListener()); callScreen.setVideoMuteButtonListener(new VideoMuteButtonListener());
callScreen.setAudioButtonListener(new AudioButtonListener()); callScreen.setSpeakerButtonListener(new SpeakerButtonListener());
callScreen.setBluetoothButtonListener(new BluetoothButtonListener());
networkAccess = new SignalServiceNetworkAccess(this); networkAccess = new SignalServiceNetworkAccess(this);
} }
@ -154,13 +144,6 @@ public class WebRtcCallActivity extends Activity {
} }
private void handleSetMuteVideo(boolean muted) { private void handleSetMuteVideo(boolean muted) {
AudioManager audioManager = ServiceUtil.getAudioManager(this);
if (!muted && !audioManager.isWiredHeadsetOn() && !audioManager.isBluetoothScoOn()) {
AudioUtils.enableSpeakerphoneRouting(WebRtcCallActivity.this);
callScreen.notifyAudioRoutingChange();
}
Intent intent = new Intent(this, WebRtcCallService.class); Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_SET_MUTE_VIDEO); intent.setAction(WebRtcCallService.ACTION_SET_MUTE_VIDEO);
intent.putExtra(WebRtcCallService.EXTRA_MUTE, muted); intent.putExtra(WebRtcCallService.EXTRA_MUTE, muted);
@ -197,12 +180,6 @@ public class WebRtcCallActivity extends Activity {
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class); Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_LOCAL_HANGUP); intent.setAction(WebRtcCallService.ACTION_LOCAL_HANGUP);
startService(intent); startService(intent);
// WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
//
// if (event != null) {
// WebRtcCallActivity.this.handleTerminate(event.getRecipient());
// }
} }
private void handleIncomingCall(@NonNull WebRtcViewModel event) { private void handleIncomingCall(@NonNull WebRtcViewModel event) {
@ -326,6 +303,8 @@ public class WebRtcCallActivity extends Activity {
callScreen.setLocalVideoEnabled(event.isLocalVideoEnabled()); callScreen.setLocalVideoEnabled(event.isLocalVideoEnabled());
callScreen.setRemoteVideoEnabled(event.isRemoteVideoEnabled()); callScreen.setRemoteVideoEnabled(event.isRemoteVideoEnabled());
callScreen.updateAudioState(event.isBluetoothAvailable(), event.isMicrophoneEnabled());
callScreen.setControlsEnabled(event.getState() != WebRtcViewModel.State.CALL_INCOMING);
} }
private class HangupButtonListener implements WebRtcCallScreen.HangupButtonListener { private class HangupButtonListener implements WebRtcCallScreen.HangupButtonListener {
@ -348,56 +327,30 @@ public class WebRtcCallActivity extends Activity {
} }
} }
private void registerBluetoothReceiver() { private class SpeakerButtonListener implements WebRtcCallControls.SpeakerButtonListener {
IntentFilter filter = new IntentFilter();
filter.addAction(AudioUtils.getScoUpdateAction());
bluetoothStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
callScreen.notifyBluetoothChange();
}
};
registerReceiver(bluetoothStateReceiver, filter);
callScreen.notifyBluetoothChange();
}
private void registerWiredHeadsetReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(AudioUtils.getWiredHeadsetUpdateAction());
wiredHeadsetStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra("state", -1);
if (state == 0 && callScreen.isVideoEnabled()) {
AudioUtils.enableSpeakerphoneRouting(WebRtcCallActivity.this);
callScreen.notifyAudioRoutingChange();
} else if (state == 1) {
AudioUtils.enableDefaultRouting(WebRtcCallActivity.this);
callScreen.notifyAudioRoutingChange();
}
}
};
registerReceiver(wiredHeadsetStateReceiver, filter);
}
private class AudioButtonListener implements WebRtcCallControls.AudioButtonListener {
@Override @Override
public void onAudioChange(AudioUtils.AudioMode mode) { public void onSpeakerChange(boolean isSpeaker) {
switch(mode) { AudioManager audioManager = ServiceUtil.getAudioManager(WebRtcCallActivity.this);
case DEFAULT: audioManager.setSpeakerphoneOn(isSpeaker);
AudioUtils.enableDefaultRouting(WebRtcCallActivity.this);
break; if (isSpeaker && audioManager.isBluetoothScoOn()) {
case SPEAKER: audioManager.stopBluetoothSco();
AudioUtils.enableSpeakerphoneRouting(WebRtcCallActivity.this); audioManager.setBluetoothScoOn(false);
break; }
case HEADSET: }
AudioUtils.enableBluetoothRouting(WebRtcCallActivity.this); }
break;
default: private class BluetoothButtonListener implements WebRtcCallControls.BluetoothButtonListener {
throw new IllegalStateException("Audio mode " + mode + " is not supported."); @Override
public void onBluetoothChange(boolean isBluetooth) {
AudioManager audioManager = ServiceUtil.getAudioManager(WebRtcCallActivity.this);
if (isBluetooth) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
} else {
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
} }
} }
} }

View File

@ -3,13 +3,11 @@ package org.thoughtcrime.securesms.components.webrtc;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.CompoundButton; import android.widget.CompoundButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@ -17,15 +15,16 @@ import android.widget.LinearLayout;
import com.tomergoldst.tooltips.ToolTip; import com.tomergoldst.tooltips.ToolTip;
import com.tomergoldst.tooltips.ToolTipsManager; import com.tomergoldst.tooltips.ToolTipsManager;
import org.thoughtcrime.redphone.util.AudioUtils;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
public class WebRtcCallControls extends LinearLayout { public class WebRtcCallControls extends LinearLayout {
private CompoundButton audioMuteButton; private CompoundButton audioMuteButton;
private CompoundButton videoMuteButton; private CompoundButton videoMuteButton;
private WebRtcInCallAudioButton audioButton; private CompoundButton speakerButton;
private CompoundButton bluetoothButton;
@TargetApi(Build.VERSION_CODES.LOLLIPOP) @TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@ -53,37 +52,10 @@ public class WebRtcCallControls extends LinearLayout {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_call_controls, this, true); inflater.inflate(R.layout.webrtc_call_controls, this, true);
this.audioMuteButton = (CompoundButton) findViewById(R.id.muteButton); this.speakerButton = ViewUtil.findById(this, R.id.speakerButton);
this.bluetoothButton = ViewUtil.findById(this, R.id.bluetoothButton);
this.audioMuteButton = ViewUtil.findById(this, R.id.muteButton);
this.videoMuteButton = ViewUtil.findById(this, R.id.video_mute_button); this.videoMuteButton = ViewUtil.findById(this, R.id.video_mute_button);
this.audioButton = new WebRtcInCallAudioButton((CompoundButton) findViewById(R.id.audioButton));
updateAudioButton();
}
public void updateAudioButton() {
audioButton.setAudioMode(AudioUtils.getCurrentAudioMode(getContext()));
IntentFilter filter = new IntentFilter();
filter.addAction(AudioUtils.getScoUpdateAction());
handleBluetoothIntent(getContext().registerReceiver(null, filter));
}
private void handleBluetoothIntent(Intent intent) {
if (intent == null) {
return;
}
if (!intent.getAction().equals(AudioUtils.getScoUpdateAction())) {
return;
}
Integer state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
if (state.equals(AudioManager.SCO_AUDIO_STATE_CONNECTED)) {
audioButton.setHeadsetAvailable(true);
} else if (state.equals(AudioManager.SCO_AUDIO_STATE_DISCONNECTED)) {
audioButton.setHeadsetAvailable(false);
}
} }
public void setAudioMuteButtonListener(final MuteButtonListener listener) { public void setAudioMuteButtonListener(final MuteButtonListener listener) {
@ -104,8 +76,45 @@ public class WebRtcCallControls extends LinearLayout {
}); });
} }
public void setAudioButtonListener(final AudioButtonListener listener) { public void setSpeakerButtonListener(final SpeakerButtonListener listener) {
audioButton.setListener(listener); speakerButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onSpeakerChange(isChecked);
updateAudioState(bluetoothButton.getVisibility() == View.VISIBLE);
}
});
}
public void setBluetoothButtonListener(final BluetoothButtonListener listener) {
bluetoothButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onBluetoothChange(isChecked);
updateAudioState(true);
}
});
}
public void updateAudioState(boolean isBluetoothAvailable) {
AudioManager audioManager = ServiceUtil.getAudioManager(getContext());
if (!isBluetoothAvailable) {
bluetoothButton.setVisibility(View.GONE);
} else {
bluetoothButton.setVisibility(View.VISIBLE);
}
if (audioManager.isBluetoothScoOn()) {
bluetoothButton.setChecked(true);
speakerButton.setChecked(false);
} else if (audioManager.isSpeakerphoneOn()) {
speakerButton.setChecked(true);
bluetoothButton.setChecked(false);
} else {
speakerButton.setChecked(false);
bluetoothButton.setChecked(false);
}
} }
public boolean isVideoEnabled() { public boolean isVideoEnabled() {
@ -116,6 +125,39 @@ public class WebRtcCallControls extends LinearLayout {
videoMuteButton.setChecked(enabled); videoMuteButton.setChecked(enabled);
} }
public void setMicrophoneEnabled(boolean enabled) {
audioMuteButton.setChecked(!enabled);
}
public void setControlsEnabled(boolean enabled) {
if (enabled && Build.VERSION.SDK_INT >= 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) { public void displayVideoTooltip(ViewGroup viewGroup) {
if (Build.VERSION.SDK_INT > 15) { if (Build.VERSION.SDK_INT > 15) {
final ToolTipsManager toolTipsManager = new ToolTipsManager(); final ToolTipsManager toolTipsManager = new ToolTipsManager();
@ -135,17 +177,23 @@ public class WebRtcCallControls extends LinearLayout {
} }
public void reset() { public void reset() {
updateAudioButton();
audioMuteButton.setChecked(false); audioMuteButton.setChecked(false);
videoMuteButton.setChecked(false); videoMuteButton.setChecked(false);
speakerButton.setChecked(false);
bluetoothButton.setChecked(false);
bluetoothButton.setVisibility(View.GONE);
} }
public static interface MuteButtonListener { public static interface MuteButtonListener {
public void onToggle(boolean isMuted); public void onToggle(boolean isMuted);
} }
public static interface AudioButtonListener { public static interface SpeakerButtonListener {
public void onAudioChange(AudioUtils.AudioMode mode); public void onSpeakerChange(boolean isSpeaker);
}
public static interface BluetoothButtonListener {
public void onBluetoothChange(boolean isBluetooth);
} }

View File

@ -163,8 +163,12 @@ public class WebRtcCallScreen extends FrameLayout implements Recipient.Recipient
this.controls.setVideoMuteButtonListener(listener); this.controls.setVideoMuteButtonListener(listener);
} }
public void setAudioButtonListener(WebRtcCallControls.AudioButtonListener listener) { public void setSpeakerButtonListener(WebRtcCallControls.SpeakerButtonListener listener) {
this.controls.setAudioButtonListener(listener); this.controls.setSpeakerButtonListener(listener);
}
public void setBluetoothButtonListener(WebRtcCallControls.BluetoothButtonListener listener) {
this.controls.setBluetoothButtonListener(listener);
} }
public void setHangupButtonListener(final HangupButtonListener listener) { public void setHangupButtonListener(final HangupButtonListener listener) {
@ -184,12 +188,13 @@ public class WebRtcCallScreen extends FrameLayout implements Recipient.Recipient
this.cancelIdentityButton.setOnClickListener(listener); this.cancelIdentityButton.setOnClickListener(listener);
} }
public void notifyBluetoothChange() { public void updateAudioState(boolean isBluetoothAvailable, boolean isMicrophoneEnabled) {
this.controls.updateAudioButton(); this.controls.updateAudioState(isBluetoothAvailable);
this.controls.setMicrophoneEnabled(isMicrophoneEnabled);
} }
public void notifyAudioRoutingChange() { public void setControlsEnabled(boolean enabled) {
this.controls.updateAudioButton(); this.controls.setControlsEnabled(enabled);
} }
public void setLocalVideoEnabled(boolean enabled) { public void setLocalVideoEnabled(boolean enabled) {

View File

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

View File

@ -32,19 +32,30 @@ public class WebRtcViewModel {
private final boolean remoteVideoEnabled; private final boolean remoteVideoEnabled;
private final boolean localVideoEnabled; private final boolean localVideoEnabled;
public WebRtcViewModel(@NonNull State state, @NonNull Recipient recipient, boolean localVideoEnabled, boolean remoteVideoEnabled) { private final boolean isBluetoothAvailable;
this(state, recipient, null, localVideoEnabled, remoteVideoEnabled); 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, public WebRtcViewModel(@NonNull State state, @NonNull Recipient recipient,
@Nullable IdentityKey identityKey, @Nullable IdentityKey identityKey,
boolean localVideoEnabled, boolean remoteVideoEnabled) boolean localVideoEnabled, boolean remoteVideoEnabled,
boolean isBluetoothAvailable, boolean isMicrophoneEnabled)
{ {
this.state = state; this.state = state;
this.recipient = recipient; this.recipient = recipient;
this.identityKey = identityKey; this.identityKey = identityKey;
this.localVideoEnabled = localVideoEnabled; this.localVideoEnabled = localVideoEnabled;
this.remoteVideoEnabled = remoteVideoEnabled; this.remoteVideoEnabled = remoteVideoEnabled;
this.isBluetoothAvailable = isBluetoothAvailable;
this.isMicrophoneEnabled = isMicrophoneEnabled;
} }
public @NonNull State getState() { public @NonNull State getState() {
@ -68,6 +79,14 @@ public class WebRtcViewModel {
return localVideoEnabled; return localVideoEnabled;
} }
public boolean isBluetoothAvailable() {
return isBluetoothAvailable;
}
public boolean isMicrophoneEnabled() {
return isMicrophoneEnabled;
}
public String toString() { public String toString() {
return "[State: " + state + ", recipient: " + recipient.getNumber() + ", identity: " + identityKey + ", remoteVideo: " + remoteVideoEnabled + ", localVideo: " + localVideoEnabled + "]"; return "[State: " + state + ", recipient: " + recipient.getNumber() + ", identity: " + identityKey + ", remoteVideo: " + remoteVideoEnabled + ", localVideo: " + localVideoEnabled + "]";
} }

View File

@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.service;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
@ -23,10 +25,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.redphone.RedPhoneService; import org.thoughtcrime.redphone.RedPhoneService;
import org.thoughtcrime.redphone.audio.IncomingRinger;
import org.thoughtcrime.redphone.call.LockManager; import org.thoughtcrime.redphone.call.LockManager;
import org.thoughtcrime.redphone.pstn.IncomingPstnCallReceiver; import org.thoughtcrime.redphone.pstn.IncomingPstnCallReceiver;
import org.thoughtcrime.redphone.util.AudioUtils;
import org.thoughtcrime.redphone.util.UncaughtExceptionHandlerManager; import org.thoughtcrime.redphone.util.UncaughtExceptionHandlerManager;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity; 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.Connected;
import org.thoughtcrime.securesms.webrtc.WebRtcDataProtos.Data; import org.thoughtcrime.securesms.webrtc.WebRtcDataProtos.Data;
import org.thoughtcrime.securesms.webrtc.WebRtcDataProtos.Hangup; 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.OutgoingRinger;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.webrtc.AudioTrack; import org.webrtc.AudioTrack;
import org.webrtc.DataChannel; import org.webrtc.DataChannel;
import org.webrtc.EglBase; import org.webrtc.EglBase;
@ -97,12 +99,11 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED; 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_INCOMING_RINGING;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_OUTGOING_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(); 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_REMOTE_NUMBER = "remote_number";
public static final String EXTRA_MUTE = "mute_value"; 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_REMOTE_DESCRIPTION = "remote_description";
public static final String EXTRA_TIMESTAMP = "timestamp"; public static final String EXTRA_TIMESTAMP = "timestamp";
public static final String EXTRA_CALL_ID = "call_id"; 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_ICE_SDP_LINE_INDEX = "ice_sdp_line_index";
public static final String EXTRA_RESULT_RECEIVER = "result_receiver"; public static final String EXTRA_RESULT_RECEIVER = "result_receiver";
public static final String ACTION_INCOMING_CALL = "CALL_INCOMING"; public static final String ACTION_INCOMING_CALL = "CALL_INCOMING";
public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING"; public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING";
public static final String ACTION_ANSWER_CALL = "ANSWER_CALL"; public static final String ACTION_ANSWER_CALL = "ANSWER_CALL";
public static final String ACTION_DENY_CALL = "DENY_CALL"; public static final String ACTION_DENY_CALL = "DENY_CALL";
public static final String ACTION_LOCAL_HANGUP = "LOCAL_HANGUP"; 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_AUDIO = "SET_MUTE_AUDIO";
public static final String ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO"; 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_BLUETOOTH_CHANGE = "BLUETOOTH_CHANGE";
public static final String ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL"; 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_RESPONSE_MESSAGE = "RESPONSE_MESSAGE";
public static final String ACTION_ICE_MESSAGE = "ICE_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"; public static final String ACTION_ICE_CONNECTED = "ICE_CONNECTED";
private CallState callState = CallState.STATE_IDLE; private CallState callState = CallState.STATE_IDLE;
private boolean audioEnabled = true; private boolean microphoneEnabled = true;
private boolean localVideoEnabled = false; private boolean localVideoEnabled = false;
private boolean remoteVideoEnabled = false; private boolean remoteVideoEnabled = false;
private boolean bluetoothAvailable = false;
private Handler serviceHandler = new Handler(); private Handler serviceHandler = new Handler();
@Inject public SignalMessageSenderFactory messageSenderFactory; @Inject public SignalMessageSenderFactory messageSenderFactory;
@ -152,8 +157,9 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
private SignalServiceMessageSender messageSender; private SignalServiceMessageSender messageSender;
private PeerConnectionFactory peerConnectionFactory; private PeerConnectionFactory peerConnectionFactory;
private IncomingRinger incomingRinger; private SignalAudioManager audioManager;
private OutgoingRinger outgoingRinger; private BluetoothStateManager bluetoothStateManager;
private WiredHeadsetStateReceiver wiredHeadsetStateReceiver;
private LockManager lockManager; private LockManager lockManager;
private IncomingPstnCallReceiver callReceiver; private IncomingPstnCallReceiver callReceiver;
@ -178,10 +184,10 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
super.onCreate(); super.onCreate();
initializeResources(); initializeResources();
initializeRingers();
registerIncomingPstnCallReceiver(); registerIncomingPstnCallReceiver();
registerUncaughtExceptionHandler(); registerUncaughtExceptionHandler();
registerWiredHeadsetStateReceiver();
} }
@Override @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_REMOTE_HANGUP)) handleRemoteHangup(intent);
else if (intent.getAction().equals(ACTION_SET_MUTE_AUDIO)) handleSetMuteAudio(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_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_REMOTE_VIDEO_MUTE)) handleRemoteVideoMute(intent);
else if (intent.getAction().equals(ACTION_RESPONSE_MESSAGE)) handleResponseMessage(intent); else if (intent.getAction().equals(ACTION_RESPONSE_MESSAGE)) handleResponseMessage(intent);
else if (intent.getAction().equals(ACTION_ICE_MESSAGE)) handleRemoteIceCandidate(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) { if (uncaughtExceptionHandlerManager != null) {
uncaughtExceptionHandlerManager.unregister(); 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 // Initializers
private void initializeRingers() {
this.outgoingRinger = new OutgoingRinger(this);
this.incomingRinger = new IncomingRinger(this);
}
private void initializeResources() { private void initializeResources() {
ApplicationContext.getInstance(this).injectDependencies(this); ApplicationContext.getInstance(this).injectDependencies(this);
this.callState = CallState.STATE_IDLE; this.callState = CallState.STATE_IDLE;
this.lockManager = new LockManager(this); this.lockManager = new LockManager(this);
this.peerConnectionFactory = new PeerConnectionFactory(new PeerConnectionFactoryOptions()); this.peerConnectionFactory = new PeerConnectionFactory(new PeerConnectionFactoryOptions());
this.audioManager = new SignalAudioManager(this);
this.bluetoothStateManager = new BluetoothStateManager(this, this);
this.messageSender = messageSenderFactory.create(); this.messageSender = messageSenderFactory.create();
this.messageSender.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10)); this.messageSender.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10));
this.accountManager.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)); 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 // Handlers
private void handleIncomingCall(final Intent intent) { 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); timeoutExecutor.schedule(new TimeoutRunnable(this.callId), 2, TimeUnit.MINUTES);
initializeVideo(); initializeVideo();
retrieveTurnServers().addListener(new SuccessOnlyListener<List<PeerConnection.IceServer>>(this.callState, this.callId) { retrieveTurnServers().addListener(new SuccessOnlyListener<List<PeerConnection.IceServer>>(this.callState, this.callId) {
@Override @Override
public void onSuccessContinue(List<PeerConnection.IceServer> result) { public void onSuccessContinue(List<PeerConnection.IceServer> result) {
@ -323,9 +362,11 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
initializeVideo(); 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); lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL);
outgoingRinger.playSonar(); audioManager.initializeAudioForCall();
audioManager.startOutgoingRinger(OutgoingRinger.Type.SONAR);
bluetoothStateManager.setWantsConnection(true);
setCallInProgressNotification(TYPE_OUTGOING_RINGING, recipient); setCallInProgressNotification(TYPE_OUTGOING_RINGING, recipient);
DatabaseFactory.getSmsDatabase(this).insertOutgoingCall(recipient.getNumber()); DatabaseFactory.getSmsDatabase(this).insertOutgoingCall(recipient.getNumber());
@ -355,11 +396,11 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
Log.w(TAG, error); Log.w(TAG, error);
if (error instanceof UntrustedIdentityException) { 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) { } 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) { } else if (error instanceof IOException) {
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled); sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
} }
terminate(); terminate();
@ -396,7 +437,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
@Override @Override
public void onFailureContinue(Throwable error) { public void onFailureContinue(Throwable error) {
Log.w(TAG, error); Log.w(TAG, error);
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled); sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
terminate(); terminate();
} }
@ -445,7 +486,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
@Override @Override
public void onFailureContinue(Throwable error) { public void onFailureContinue(Throwable error) {
Log.w(TAG, error); Log.w(TAG, error);
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled); sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, recipient, localVideoEnabled, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
terminate(); terminate();
} }
@ -459,17 +500,19 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
this.callState = CallState.STATE_LOCAL_RINGING; this.callState = CallState.STATE_LOCAL_RINGING;
this.lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE); 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(); startCallCardActivity();
incomingRinger.start(); audioManager.initializeAudioForCall();
audioManager.startIncomingRinger();
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient); setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient);
} else if (callState == CallState.STATE_DIALING) { } else if (callState == CallState.STATE_DIALING) {
if (this.recipient == null) throw new AssertionError("assert"); if (this.recipient == null) throw new AssertionError("assert");
this.callState = CallState.STATE_REMOTE_RINGING; 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"); throw new AssertionError("assert");
} }
callState = CallState.STATE_CONNECTED; audioManager.startCommunication(callState == CallState.STATE_REMOTE_RINGING);
bluetoothStateManager.setWantsConnection(true);
initializeAudio(); callState = CallState.STATE_CONNECTED;
outgoingRinger.playComplete();
if (localVideoEnabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO); if (localVideoEnabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO);
else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL); 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); setCallInProgressNotification(TYPE_ESTABLISHED, recipient);
this.peerConnection.setAudioEnabled(audioEnabled); this.peerConnection.setAudioEnabled(microphoneEnabled);
this.peerConnection.setVideoEnabled(localVideoEnabled); this.peerConnection.setVideoEnabled(localVideoEnabled);
this.dataChannel.send(new DataChannel.Buffer(ByteBuffer.wrap(Data.newBuilder() this.dataChannel.send(new DataChannel.Buffer(ByteBuffer.wrap(Data.newBuilder()
@ -529,9 +572,9 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return; 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() { serviceHandler.postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -546,7 +589,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
this.callState != CallState.STATE_CONNECTED) this.callState != CallState.STATE_CONNECTED)
{ {
Log.w(TAG, "Timing out call: " + this.callId); 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(); terminate();
} }
} }
@ -575,8 +618,6 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
throw new AssertionError("assert"); throw new AssertionError("assert");
} }
incomingRinger.stop();
DatabaseFactory.getSmsDatabase(this).insertReceivedCall(recipient.getNumber()); DatabaseFactory.getSmsDatabase(this).insertReceivedCall(recipient.getNumber());
this.peerConnection.setAudioEnabled(true); 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)); 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(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(); 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) { 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 { } 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) { 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) { private void handleSetMuteAudio(Intent intent) {
boolean muted = intent.getBooleanExtra(EXTRA_MUTE, false); boolean muted = intent.getBooleanExtra(EXTRA_MUTE, false);
this.audioEnabled = !muted; this.microphoneEnabled = !muted;
if (this.peerConnection != null) { if (this.peerConnection != null) {
this.peerConnection.setAudioEnabled(this.audioEnabled); this.peerConnection.setAudioEnabled(this.microphoneEnabled);
} }
} }
private void handleSetMuteVideo(Intent intent) { 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; this.localVideoEnabled = !muted;
if (this.peerConnection != null) { if (this.peerConnection != null) {
@ -672,7 +715,42 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
else this.lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL); 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) { private void handleRemoteVideoMute(Intent intent) {
@ -685,7 +763,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
} }
this.remoteVideoEnabled = !muted; 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 /// 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); 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() { private void initializeVideo() {
Util.runOnMainSync(new Runnable() { Util.runOnMainSync(new Runnable() {
@Override @Override
@ -737,22 +806,12 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient)); 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() { private synchronized void terminate() {
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING); lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING);
stopForeground(true); stopForeground(true);
incomingRinger.stop(); audioManager.stop(callState == CallState.STATE_DIALING || callState == CallState.STATE_REMOTE_RINGING || callState == CallState.STATE_CONNECTED);
outgoingRinger.stop(); bluetoothStateManager.setWantsConnection(false);
outgoingRinger.playDisconnected();
if (peerConnection != null) { if (peerConnection != null) {
peerConnection.dispose(); peerConnection.dispose();
@ -769,12 +828,10 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
eglBase = null; eglBase = null;
} }
shutdownAudio();
this.callState = CallState.STATE_IDLE; this.callState = CallState.STATE_IDLE;
this.recipient = null; this.recipient = null;
this.callId = null; this.callId = null;
this.audioEnabled = true; this.microphoneEnabled = true;
this.localVideoEnabled = false; this.localVideoEnabled = false;
this.remoteVideoEnabled = false; this.remoteVideoEnabled = false;
this.pendingIceUpdates = null; this.pendingIceUpdates = null;
@ -784,17 +841,19 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
private void sendMessage(@NonNull WebRtcViewModel.State state, private void sendMessage(@NonNull WebRtcViewModel.State state,
@NonNull Recipient recipient, @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, private void sendMessage(@NonNull WebRtcViewModel.State state,
@NonNull Recipient recipient, @NonNull Recipient recipient,
@NonNull IdentityKey identityKey, @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<Boolean> sendMessage(@NonNull final Recipient recipient, private ListenableFutureTask<Boolean> sendMessage(@NonNull final Recipient recipient,
@ -1022,6 +1081,8 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return futureTask; return futureTask;
} }
////
private WebRtcViewModel.State viewModelStateFor(CallState state) { private WebRtcViewModel.State viewModelStateFor(CallState state) {
switch (state) { switch (state) {
case STATE_CONNECTED: return WebRtcViewModel.State.CALL_CONNECTED; 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; 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 class TimeoutRunnable implements Runnable {
private final long callId; private final long callId;

View File

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

View File

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

View File

@ -4,84 +4,56 @@ import android.content.Context;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.io.IOException; import java.io.IOException;
/** public class OutgoingRinger {
* 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 {
private static final String TAG = OutgoingRinger.class.getSimpleName(); private static final String TAG = OutgoingRinger.class.getSimpleName();
public enum Type {
SONAR,
RINGING,
BUSY
}
private final Context context;
private MediaPlayer mediaPlayer; 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 playSonar() { public void start(Type type) {
start(R.raw.redphone_sonarping); int soundId;
}
public void playRing() { if (type == Type.SONAR) soundId = R.raw.redphone_sonarping;
start(R.raw.redphone_outring); 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 playComplete() { if( mediaPlayer != null ) {
stop(R.raw.webrtc_completed); mediaPlayer.release();
} }
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 = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL); mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
mediaPlayer.setOnCompletionListener(this); mediaPlayer.setLooping(true);
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.setLooping(loopEnabled);
String packageName = context.getPackageName(); String packageName = context.getPackageName();
Uri dataUri = Uri.parse("android.resource://" + packageName + "/" + currentSoundID); Uri dataUri = Uri.parse("android.resource://" + packageName + "/" + soundId);
try { try {
mediaPlayer.setDataSource(context, dataUri); mediaPlayer.setDataSource(context, dataUri);
mediaPlayer.prepareAsync(); mediaPlayer.prepare();
mediaPlayer.start();
} catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) { } catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) {
Log.w(TAG, 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; if (mediaPlayer == null) return;
mediaPlayer.release(); mediaPlayer.release();
mediaPlayer = null; 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);
}
} }
} }

View File

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