mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
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:
parent
3904c76261
commit
cd28cd172f
BIN
res/drawable-hdpi/ic_bluetooth_white_24dp.png
Normal file
BIN
res/drawable-hdpi/ic_bluetooth_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 307 B |
BIN
res/drawable-mdpi/ic_bluetooth_white_24dp.png
Normal file
BIN
res/drawable-mdpi/ic_bluetooth_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 213 B |
BIN
res/drawable-xhdpi/ic_bluetooth_white_24dp.png
Normal file
BIN
res/drawable-xhdpi/ic_bluetooth_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 344 B |
BIN
res/drawable-xxhdpi/ic_bluetooth_white_24dp.png
Normal file
BIN
res/drawable-xxhdpi/ic_bluetooth_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 502 B |
BIN
res/drawable-xxxhdpi/ic_bluetooth_white_24dp.png
Normal file
BIN
res/drawable-xxxhdpi/ic_bluetooth_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 595 B |
@ -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>
|
||||
|
||||
<!--<!– Speakerphone icon showing 'speaker off' state –>-->
|
||||
<!--<item android:id="@+id/speakerphoneOffItem">-->
|
||||
<!--<bitmap android:src="@drawable/ic_volume_mute_white_24dp"-->
|
||||
<!--android:gravity="center" />-->
|
||||
<!--</item>-->
|
||||
|
||||
</layer-list>
|
9
res/drawable/webrtc_bluetooth_button.xml
Normal file
9
res/drawable/webrtc_bluetooth_button.xml
Normal 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>
|
9
res/drawable/webrtc_speaker_button.xml
Normal file
9
res/drawable/webrtc_speaker_button.xml
Normal 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>
|
@ -7,12 +7,18 @@
|
||||
android:orientation="horizontal"
|
||||
tools:background="@color/textsecure_primary">
|
||||
|
||||
<ToggleButton android:id="@+id/audioButton"
|
||||
<ToggleButton android:id="@+id/speakerButton"
|
||||
style="@style/WebRtcCallCompoundButton"
|
||||
android:background="@drawable/webrtc_audio_button"
|
||||
android:background="@drawable/webrtc_speaker_button"
|
||||
tools:checked="true"
|
||||
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"
|
||||
style="@style/WebRtcCallCompoundButton"
|
||||
|
@ -176,7 +176,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/textsecure_primary"
|
||||
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="16dp"
|
||||
|
@ -18,12 +18,9 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
@ -38,7 +35,6 @@ import android.view.WindowManager;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.redphone.util.AudioUtils;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
|
||||
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";
|
||||
|
||||
private WebRtcCallScreen callScreen;
|
||||
private BroadcastReceiver bluetoothStateReceiver;
|
||||
private BroadcastReceiver wiredHeadsetStateReceiver;
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
|
||||
@Override
|
||||
@ -93,9 +87,6 @@ public class WebRtcCallActivity extends Activity {
|
||||
if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStarted(this);
|
||||
initializeScreenshotSecurity();
|
||||
EventBus.getDefault().register(this);
|
||||
|
||||
registerBluetoothReceiver();
|
||||
registerWiredHeadsetReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -116,8 +107,6 @@ public class WebRtcCallActivity extends Activity {
|
||||
super.onPause();
|
||||
if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStopped(this);
|
||||
EventBus.getDefault().unregister(this);
|
||||
unregisterReceiver(bluetoothStateReceiver);
|
||||
unregisterReceiver(wiredHeadsetStateReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -141,7 +130,8 @@ public class WebRtcCallActivity extends Activity {
|
||||
callScreen.setIncomingCallActionListener(new IncomingCallActionListener());
|
||||
callScreen.setAudioMuteButtonListener(new AudioMuteButtonListener());
|
||||
callScreen.setVideoMuteButtonListener(new VideoMuteButtonListener());
|
||||
callScreen.setAudioButtonListener(new AudioButtonListener());
|
||||
callScreen.setSpeakerButtonListener(new SpeakerButtonListener());
|
||||
callScreen.setBluetoothButtonListener(new BluetoothButtonListener());
|
||||
|
||||
networkAccess = new SignalServiceNetworkAccess(this);
|
||||
}
|
||||
@ -154,13 +144,6 @@ public class WebRtcCallActivity extends Activity {
|
||||
}
|
||||
|
||||
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.setAction(WebRtcCallService.ACTION_SET_MUTE_VIDEO);
|
||||
intent.putExtra(WebRtcCallService.EXTRA_MUTE, muted);
|
||||
@ -197,12 +180,6 @@ public class WebRtcCallActivity extends Activity {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_LOCAL_HANGUP);
|
||||
startService(intent);
|
||||
|
||||
// WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
|
||||
//
|
||||
// if (event != null) {
|
||||
// WebRtcCallActivity.this.handleTerminate(event.getRecipient());
|
||||
// }
|
||||
}
|
||||
|
||||
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
|
||||
@ -326,6 +303,8 @@ public class WebRtcCallActivity extends Activity {
|
||||
|
||||
callScreen.setLocalVideoEnabled(event.isLocalVideoEnabled());
|
||||
callScreen.setRemoteVideoEnabled(event.isRemoteVideoEnabled());
|
||||
callScreen.updateAudioState(event.isBluetoothAvailable(), event.isMicrophoneEnabled());
|
||||
callScreen.setControlsEnabled(event.getState() != WebRtcViewModel.State.CALL_INCOMING);
|
||||
}
|
||||
|
||||
private class HangupButtonListener implements WebRtcCallScreen.HangupButtonListener {
|
||||
@ -348,56 +327,30 @@ public class WebRtcCallActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private void registerBluetoothReceiver() {
|
||||
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 {
|
||||
private class SpeakerButtonListener implements WebRtcCallControls.SpeakerButtonListener {
|
||||
@Override
|
||||
public void onAudioChange(AudioUtils.AudioMode mode) {
|
||||
switch(mode) {
|
||||
case DEFAULT:
|
||||
AudioUtils.enableDefaultRouting(WebRtcCallActivity.this);
|
||||
break;
|
||||
case SPEAKER:
|
||||
AudioUtils.enableSpeakerphoneRouting(WebRtcCallActivity.this);
|
||||
break;
|
||||
case HEADSET:
|
||||
AudioUtils.enableBluetoothRouting(WebRtcCallActivity.this);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Audio mode " + mode + " is not supported.");
|
||||
public void onSpeakerChange(boolean isSpeaker) {
|
||||
AudioManager audioManager = ServiceUtil.getAudioManager(WebRtcCallActivity.this);
|
||||
audioManager.setSpeakerphoneOn(isSpeaker);
|
||||
|
||||
if (isSpeaker && audioManager.isBluetoothScoOn()) {
|
||||
audioManager.stopBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class BluetoothButtonListener implements WebRtcCallControls.BluetoothButtonListener {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,11 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Color;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
@ -17,15 +15,16 @@ import android.widget.LinearLayout;
|
||||
import com.tomergoldst.tooltips.ToolTip;
|
||||
import com.tomergoldst.tooltips.ToolTipsManager;
|
||||
|
||||
import org.thoughtcrime.redphone.util.AudioUtils;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class WebRtcCallControls extends LinearLayout {
|
||||
|
||||
private CompoundButton audioMuteButton;
|
||||
private CompoundButton videoMuteButton;
|
||||
private WebRtcInCallAudioButton audioButton;
|
||||
private CompoundButton audioMuteButton;
|
||||
private CompoundButton videoMuteButton;
|
||||
private CompoundButton speakerButton;
|
||||
private CompoundButton bluetoothButton;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
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);
|
||||
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.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) {
|
||||
@ -104,8 +76,45 @@ public class WebRtcCallControls extends LinearLayout {
|
||||
});
|
||||
}
|
||||
|
||||
public void setAudioButtonListener(final AudioButtonListener listener) {
|
||||
audioButton.setListener(listener);
|
||||
public void setSpeakerButtonListener(final SpeakerButtonListener 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() {
|
||||
@ -116,6 +125,39 @@ public class WebRtcCallControls extends LinearLayout {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
|
@ -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<List<PeerConnection.IceServer>>(this.callState, this.callId) {
|
||||
@Override
|
||||
public void onSuccessContinue(List<PeerConnection.IceServer> 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<Boolean> 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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
142
src/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java
Normal file
142
src/org/thoughtcrime/securesms/webrtc/audio/IncomingRinger.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 playSonar() {
|
||||
start(R.raw.redphone_sonarping);
|
||||
}
|
||||
public void start(Type type) {
|
||||
int soundId;
|
||||
|
||||
public void playRing() {
|
||||
start(R.raw.redphone_outring);
|
||||
}
|
||||
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 playComplete() {
|
||||
stop(R.raw.webrtc_completed);
|
||||
}
|
||||
if( mediaPlayer != null ) {
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user