From a3bbf944e5a713ba540ee2ce81abf955a1d055a0 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 13 May 2022 12:39:23 -0400 Subject: [PATCH] Handle bluetooth permission crash during calls. --- .../thoughtcrime/securesms/WebRtcCallActivity.java | 14 ++++++++++++++ .../securesms/events/WebRtcViewModel.kt | 2 ++ .../service/webrtc/SignalCallManager.java | 4 ++++ .../service/webrtc/WebRtcActionProcessor.java | 7 +++++++ .../service/webrtc/WebRtcCallService.java | 5 +++++ .../service/webrtc/state/LocalDeviceState.kt | 3 ++- .../webrtc/state/WebRtcServiceStateBuilder.java | 5 +++++ .../securesms/webrtc/audio/SignalAudioManager.kt | 7 +++++++ .../webrtc/audio/SignalBluetoothManager.kt | 12 +++++++++++- app/src/main/res/values/strings.xml | 8 ++++++++ 10 files changed, 65 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 1812a72d3f..ac838c6c9c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -44,6 +44,8 @@ import androidx.window.DisplayFeature; import androidx.window.FoldingFeature; import androidx.window.WindowLayoutInfo; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -112,6 +114,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan private TooltipPopup videoTooltip; private WebRtcCallViewModel viewModel; private boolean enableVideoIfAvailable; + private boolean hasWarnedAboutBluetooth; private androidx.window.WindowManager windowManager; private WindowLayoutInfoConsumer windowLayoutInfoConsumer; private ThrottledDebouncer requestNewSizesThrottle; @@ -686,6 +689,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan enableVideoIfAvailable = false; handleSetMuteVideo(false); } + + if (event.getBluetoothPermissionDenied() && !hasWarnedAboutBluetooth && !isFinishing()) { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.WebRtcCallActivity__bluetooth_permission_denied) + .setMessage(R.string.WebRtcCallActivity__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call) + .setPositiveButton(R.string.WebRtcCallActivity__open_settings, (d, w) -> startActivity(Permissions.getApplicationSettingsIntent(this))) + .setNegativeButton(R.string.WebRtcCallActivity__not_now, null) + .show(); + + hasWarnedAboutBluetooth = true; + } } private void handleCallPreJoin(@NonNull WebRtcViewModel event) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt index 77977d3ac8..569525f834 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt @@ -96,6 +96,7 @@ class WebRtcViewModel(state: WebRtcServiceState) { val ringerRecipient: Recipient = state.getCallSetupState(state.callInfoState.activePeer?.callId).ringerRecipient val activeDevice: SignalAudioManager.AudioDevice = state.localDeviceState.activeDevice val availableDevices: Set = state.localDeviceState.availableDevices + val bluetoothPermissionDenied: Boolean = state.localDeviceState.bluetoothPermissionDenied val localParticipant: CallParticipant = createLocal( state.localDeviceState.cameraState, @@ -124,6 +125,7 @@ class WebRtcViewModel(state: WebRtcServiceState) { participantLimit=$participantLimit, activeDevice=$activeDevice, availableDevices=$availableDevices, + bluetoothPermissionDenied=$bluetoothPermissionDenied, ringGroup=$ringGroup } """.trimIndent() diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 2dc7603f56..9feac0037d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -309,6 +309,10 @@ private void processStateless(@NonNull Function1 p.handleAudioDeviceChanged(s, activeDevice, availableDevices)); } + public void onBluetoothPermissionDenied() { + process((s, p) -> p.handleBluetoothPermissionDenied(s)); + } + public void selectAudioDevice(@NonNull SignalAudioManager.AudioDevice desiredDevice) { process((s, p) -> p.handleSetUserAudioDevice(s, desiredDevice)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 61c2fce378..f35ad732ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -457,6 +457,13 @@ public abstract class WebRtcActionProcessor { return currentState; } + public @NonNull WebRtcServiceState handleBluetoothPermissionDenied(@NonNull WebRtcServiceState currentState) { + return currentState.builder() + .changeLocalDeviceState() + .setBluetoothPermissionDenied(true) + .build(); + } + protected @NonNull WebRtcServiceState handleSetUserAudioDevice(@NonNull WebRtcServiceState currentState, @NonNull SignalAudioManager.AudioDevice userDevice) { Log.i(tag, "handleSetUserAudioDevice not processed"); return currentState; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java index 8c86df16b5..a49f3b4b72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java @@ -267,6 +267,11 @@ public final class WebRtcCallService extends Service implements SignalAudioManag callManager.onAudioDeviceChanged(activeDevice, availableDevices); } + @Override + public void onBluetoothPermissionDenied() { + callManager.onBluetoothPermissionDenied(); + } + private class HangUpRtcOnPstnCallAnsweredListener extends PhoneStateListener { @Override public void onCallStateChanged(int state, @NonNull String phoneNumber) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt index c558d65445..bf53a7d63f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt @@ -14,7 +14,8 @@ data class LocalDeviceState constructor( var isLandscapeEnabled: Boolean = false, var deviceOrientation: Orientation = Orientation.PORTRAIT_BOTTOM_EDGE, var activeDevice: SignalAudioManager.AudioDevice = SignalAudioManager.AudioDevice.NONE, - var availableDevices: Set = emptySet() + var availableDevices: Set = emptySet(), + var bluetoothPermissionDenied: Boolean = false ) { fun duplicate(): LocalDeviceState { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java index 7498817037..f8f7a9f5d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java @@ -125,6 +125,11 @@ public class WebRtcServiceStateBuilder { toBuild.setAvailableDevices(availableDevices); return this; } + + public @NonNull LocalDeviceStateBuilder setBluetoothPermissionDenied(boolean bluetoothPermissionDenied) { + toBuild.setBluetoothPermissionDenied(bluetoothPermissionDenied); + return this; + } } public class CallSetupStateBuilder { diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt index c819058276..7437d3a75e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt @@ -98,6 +98,7 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev interface EventListener { @JvmSuppressWildcards fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set) + fun onBluetoothPermissionDenied() } } @@ -121,6 +122,7 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) : private var audioDevices: MutableSet = mutableSetOf() private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE + private var previousBluetoothState: SignalBluetoothManager.State? = null private var savedAudioMode = AudioManager.MODE_INVALID private var savedIsSpeakerPhoneOn = false @@ -294,6 +296,11 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) : autoSwitchToBluetooth = false } + if (previousBluetoothState != null && previousBluetoothState != SignalBluetoothManager.State.PERMISSION_DENIED && signalBluetoothManager.state == SignalBluetoothManager.State.PERMISSION_DENIED) { + eventListener?.onBluetoothPermissionDenied() + } + previousBluetoothState = signalBluetoothManager.state + val newAudioDevice: AudioDevice = when { audioDevices.contains(userSelectedAudioDevice) -> userSelectedAudioDevice audioDevices.contains(defaultAudioDevice) -> defaultAudioDevice diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt index 53b6918911..df99493533 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalBluetoothManager.kt @@ -162,7 +162,16 @@ class SignalBluetoothManager( return } - val devices: List? = bluetoothHeadset?.connectedDevices + val devices: List? + try { + devices = bluetoothHeadset?.connectedDevices + } catch (e: SecurityException) { + Log.w(TAG, "Unable to get bluetooth devices", e) + stop() + state = State.PERMISSION_DENIED + return + } + if (devices == null || devices.isEmpty()) { bluetoothDevice = null state = State.UNAVAILABLE @@ -320,6 +329,7 @@ class SignalBluetoothManager( DISCONNECTING, CONNECTING, CONNECTED, + PERMISSION_DENIED, ERROR; fun shouldUpdate(): Boolean { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b98ffc821f..ee9b9af135 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1499,6 +1499,14 @@ Group is too large to ring the participants. Reconnecting… + + Bluetooth permission denied + + Please enable the \"Nearby devices\" permission to use bluetooth during a call. + + Open settings + + Not now Signal Call