diff --git a/app/build.gradle b/app/build.gradle index bf0c91444..900d50f30 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,7 +39,7 @@ android { buildToolsVersion '33.0.0' defaultConfig { minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index afb39eef3..e3255e82a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,8 +37,11 @@ - + + @@ -122,6 +126,8 @@ + + @@ -180,7 +186,8 @@ android:name=".messagesearch.MessageSearchActivity" android:theme="@style/AppTheme" /> - + @@ -203,7 +210,8 @@ android:resource="@xml/contacts" /> - + diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index 1b78b4714..e554d2dd4 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -30,6 +30,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.drawable.Icon; @@ -93,7 +94,7 @@ import com.nextcloud.talk.utils.database.user.UserUtils; import com.nextcloud.talk.utils.power.PowerManagerUtils; import com.nextcloud.talk.utils.preferences.AppPreferences; import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder; -import com.nextcloud.talk.webrtc.MagicAudioManager; +import com.nextcloud.talk.webrtc.WebRtcAudioManger; import com.nextcloud.talk.webrtc.MagicWebRTCUtils; import com.nextcloud.talk.webrtc.MagicWebSocketInstance; import com.nextcloud.talk.webrtc.PeerConnectionWrapper; @@ -138,12 +139,15 @@ import java.util.concurrent.TimeUnit; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import autodagger.AutoInjector; import io.reactivex.Observable; @@ -157,6 +161,7 @@ import me.zhanghai.android.effortlesspermissions.OpenAppDetailsDialogFragment; import okhttp3.Cache; import pub.devrel.easypermissions.AfterPermissionGranted; +import static android.app.PendingIntent.FLAG_MUTABLE; import static com.nextcloud.talk.webrtc.Globals.JOB_ID; import static com.nextcloud.talk.webrtc.Globals.PARTICIPANTS_UPDATE; import static com.nextcloud.talk.webrtc.Globals.ROOM_TOKEN; @@ -182,11 +187,11 @@ public class CallActivity extends CallBaseActivity { public static final String TAG = "CallActivity"; - public MagicAudioManager audioManager; + public WebRtcAudioManger audioManager; private static final String[] PERMISSIONS_CALL = { - android.Manifest.permission.CAMERA, - android.Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO }; private static final String[] PERMISSIONS_CAMERA = { @@ -268,6 +273,13 @@ public class CallActivity extends CallBaseActivity { private AudioOutputDialog audioOutputDialog; + private final ActivityResultLauncher requestBluetoothPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + enableBluetoothManager(); + } + }); + @SuppressLint("ClickableViewAccessibility") @Override public void onCreate(Bundle savedInstanceState) { @@ -317,6 +329,9 @@ public class CallActivity extends CallBaseActivity { .setRepeatCount(PulseAnimation.INFINITE) .setRepeatMode(PulseAnimation.REVERSE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + requestBluetoothPermission(); + } basicInitialization(); participantDisplayItems = new HashMap<>(); initViews(); @@ -326,6 +341,22 @@ public class CallActivity extends CallBaseActivity { updateSelfVideoViewPosition(); } + @SuppressLint("InlinedApi") + @RequiresApi(api = Build.VERSION_CODES.S) + private void requestBluetoothPermission() { + if (ContextCompat.checkSelfPermission( + getContext(), Manifest.permission.BLUETOOTH_CONNECT) == + PackageManager.PERMISSION_DENIED) { + requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT); + } + } + + private void enableBluetoothManager() { + if (audioManager != null) { + audioManager.startBluetoothManager(); + } + } + @Override public void onStart() { super.onStart(); @@ -419,16 +450,16 @@ public class CallActivity extends CallBaseActivity { // Create and audio manager that will take care of audio routing, // audio modes, audio device enumeration etc. - audioManager = MagicAudioManager.create(getApplicationContext(), isVoiceOnlyCall); + audioManager = WebRtcAudioManger.create(getApplicationContext(), isVoiceOnlyCall); // Store existing audio settings and change audio mode to // MODE_IN_COMMUNICATION for best possible VoIP performance. Log.d(TAG, "Starting the audio manager..."); audioManager.start(this::onAudioManagerDevicesChanged); if (isVoiceOnlyCall) { - setAudioOutputChannel(MagicAudioManager.AudioDevice.EARPIECE); + setAudioOutputChannel(WebRtcAudioManger.AudioDevice.EARPIECE); } else { - setAudioOutputChannel(MagicAudioManager.AudioDevice.SPEAKER_PHONE); + setAudioOutputChannel(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE); } iceServers = new ArrayList<>(); @@ -462,14 +493,14 @@ public class CallActivity extends CallBaseActivity { microphoneInitialization(); } - public void setAudioOutputChannel(MagicAudioManager.AudioDevice selectedAudioDevice) { + public void setAudioOutputChannel(WebRtcAudioManger.AudioDevice selectedAudioDevice) { if (audioManager != null) { audioManager.selectAudioDevice(selectedAudioDevice); updateAudioOutputButton(audioManager.getCurrentAudioDevice()); } } - private void updateAudioOutputButton(MagicAudioManager.AudioDevice activeAudioDevice) { + private void updateAudioOutputButton(WebRtcAudioManger.AudioDevice activeAudioDevice) { switch (activeAudioDevice) { case BLUETOOTH: binding.audioOutputButton.getHierarchy().setPlaceholderImage( @@ -763,14 +794,14 @@ public class CallActivity extends CallBaseActivity { } private void onAudioManagerDevicesChanged( - final MagicAudioManager.AudioDevice currentDevice, - final Set availableDevices) { + final WebRtcAudioManger.AudioDevice currentDevice, + final Set availableDevices) { Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", " + "currentDevice: " + currentDevice); - final boolean shouldDisableProximityLock = (currentDevice.equals(MagicAudioManager.AudioDevice.WIRED_HEADSET) - || currentDevice.equals(MagicAudioManager.AudioDevice.SPEAKER_PHONE) - || currentDevice.equals(MagicAudioManager.AudioDevice.BLUETOOTH)); + final boolean shouldDisableProximityLock = (currentDevice.equals(WebRtcAudioManger.AudioDevice.WIRED_HEADSET) + || currentDevice.equals(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE) + || currentDevice.equals(WebRtcAudioManger.AudioDevice.BLUETOOTH)); if (shouldDisableProximityLock) { powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK); @@ -2580,7 +2611,7 @@ public class CallActivity extends CallBaseActivity { this, requestCode, new Intent(MICROPHONE_PIP_INTENT_NAME).putExtra(MICROPHONE_PIP_INTENT_EXTRA_ACTION, requestCode), - 0); + FLAG_MUTABLE); actions.add(new RemoteAction(icon, title, title, intent)); diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt index 2876745eb..ff394bcd8 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt @@ -30,7 +30,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.nextcloud.talk.R import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.databinding.DialogAudioOutputBinding -import com.nextcloud.talk.webrtc.MagicAudioManager +import com.nextcloud.talk.webrtc.WebRtcAudioManger class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(callActivity) { @@ -47,26 +47,26 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call } fun updateOutputDeviceList() { - if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.BLUETOOTH) == false) { + if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManger.AudioDevice.BLUETOOTH) == false) { dialogAudioOutputBinding.audioOutputBluetooth.visibility = View.GONE } else { dialogAudioOutputBinding.audioOutputBluetooth.visibility = View.VISIBLE } - if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.EARPIECE) == false) { + if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManger.AudioDevice.EARPIECE) == false) { dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.GONE } else { dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.VISIBLE } - if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.SPEAKER_PHONE) == false) { + if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE) == false) { dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.GONE } else { dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.VISIBLE } if (callActivity.audioManager?.currentAudioDevice?.equals( - MagicAudioManager.AudioDevice.WIRED_HEADSET + WebRtcAudioManger.AudioDevice.WIRED_HEADSET ) == true ) { dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.GONE @@ -81,7 +81,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call private fun highlightActiveOutputChannel() { when (callActivity.audioManager?.currentAudioDevice) { - MagicAudioManager.AudioDevice.BLUETOOTH -> { + WebRtcAudioManger.AudioDevice.BLUETOOTH -> { dialogAudioOutputBinding.audioOutputBluetoothIcon.setColorFilter( ContextCompat.getColor( context, @@ -94,7 +94,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call ) } - MagicAudioManager.AudioDevice.SPEAKER_PHONE -> { + WebRtcAudioManger.AudioDevice.SPEAKER_PHONE -> { dialogAudioOutputBinding.audioOutputSpeakerIcon.setColorFilter( ContextCompat.getColor( context, @@ -107,7 +107,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call ) } - MagicAudioManager.AudioDevice.EARPIECE -> { + WebRtcAudioManger.AudioDevice.EARPIECE -> { dialogAudioOutputBinding.audioOutputEarspeakerIcon.setColorFilter( ContextCompat.getColor( context, @@ -120,7 +120,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call ) } - MagicAudioManager.AudioDevice.WIRED_HEADSET -> { + WebRtcAudioManger.AudioDevice.WIRED_HEADSET -> { dialogAudioOutputBinding.audioOutputWiredHeadsetIcon.setColorFilter( ContextCompat.getColor( context, @@ -139,17 +139,17 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call private fun initClickListeners() { dialogAudioOutputBinding.audioOutputBluetooth.setOnClickListener { - callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.BLUETOOTH) + callActivity.setAudioOutputChannel(WebRtcAudioManger.AudioDevice.BLUETOOTH) dismiss() } dialogAudioOutputBinding.audioOutputSpeaker.setOnClickListener { - callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.SPEAKER_PHONE) + callActivity.setAudioOutputChannel(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE) dismiss() } dialogAudioOutputBinding.audioOutputEarspeaker.setOnClickListener { - callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.EARPIECE) + callActivity.setAudioOutputChannel(WebRtcAudioManger.AudioDevice.EARPIECE) dismiss() } } diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManger.java similarity index 90% rename from app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java rename to app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManger.java index 866e5c6ce..0bf784d49 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManger.java @@ -2,6 +2,8 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Tim Krüger + * Copyright (C) 2022 Tim Krüger * Copyright (C) 2017 Mario Danic * * This program is free software: you can redistribute it and/or modify @@ -52,15 +54,12 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -/** - * MagicAudioManager manages all audio related parts of the AppRTC demo. - */ -public class MagicAudioManager { - private static final String TAG = "MagicAudioManager"; +public class WebRtcAudioManger { + private static final String TAG = WebRtcAudioManger.class.getCanonicalName(); private final Context magicContext; - private final MagicBluetoothManager bluetoothManager; - private boolean useProximitySensor; - private AudioManager audioManager; + private final WebRtcBluetoothManager bluetoothManager; + private final boolean useProximitySensor; + private final AudioManager audioManager; private AudioManagerListener audioManagerListener; private AudioManagerState amState; private int savedAudioMode = AudioManager.MODE_INVALID; @@ -75,17 +74,17 @@ public class MagicAudioManager { private Set audioDevices = new HashSet<>(); - private BroadcastReceiver wiredHeadsetReceiver; + private final BroadcastReceiver wiredHeadsetReceiver; private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; - private PowerManagerUtils powerManagerUtils; + private final PowerManagerUtils powerManagerUtils; - private MagicAudioManager(Context context, boolean useProximitySensor) { + private WebRtcAudioManger(Context context, boolean useProximitySensor) { Log.d(TAG, "ctor"); ThreadUtils.checkIsOnMainThread(); magicContext = context; audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); - bluetoothManager = MagicBluetoothManager.create(context, this); + bluetoothManager = WebRtcBluetoothManager.create(context, this); wiredHeadsetReceiver = new WiredHeadsetReceiver(); amState = AudioManagerState.UNINITIALIZED; @@ -111,8 +110,14 @@ public class MagicAudioManager { /** * Construction. */ - public static MagicAudioManager create(Context context, boolean useProximitySensor) { - return new MagicAudioManager(context, useProximitySensor); + public static WebRtcAudioManger create(Context context, boolean useProximitySensor) { + return new WebRtcAudioManger(context, useProximitySensor); + } + + public void startBluetoothManager() { + // Initialize and start Bluetooth if a BT device is available or initiate + // detection of new (enabled) BT devices. + bluetoothManager.start(); } /** @@ -136,7 +141,7 @@ public class MagicAudioManager { .SENSOR_NEAR, null, null, null, null)); } else { - setAudioDeviceInternal(MagicAudioManager.AudioDevice.SPEAKER_PHONE); + setAudioDeviceInternal(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE); Log.d(TAG, "switched to SPEAKER_PHONE because userSelectedAudioDevice was SPEAKER_PHONE and proximity=far"); EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType @@ -228,9 +233,7 @@ public class MagicAudioManager { currentAudioDevice = AudioDevice.NONE; audioDevices.clear(); - // Initialize and start Bluetooth if a BT device is available or initiate - // detection of new (enabled) BT devices. - bluetoothManager.start(); + startBluetoothManager(); // Do initial selection of audio device. This setting can later be changed // either by adding/removing a BT or wired headset or by covering/uncovering @@ -256,7 +259,9 @@ public class MagicAudioManager { unregisterReceiver(wiredHeadsetReceiver); - bluetoothManager.stop(); + if(bluetoothManager.started()) { + bluetoothManager.stop(); + } // Restore previously stored audio states. setSpeakerphoneOn(savedIsSpeakerPhoneOn); @@ -411,17 +416,17 @@ public class MagicAudioManager { + "current=" + currentAudioDevice + ", " + "user selected=" + userSelectedAudioDevice); - if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE - || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) { + if (bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE + || bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_UNAVAILABLE + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_DISCONNECTING) { bluetoothManager.updateDevice(); } Set newAudioDevices = new HashSet<>(); - if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING - || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE) { + if (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING + || bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE) { newAudioDevices.add(AudioDevice.BLUETOOTH); } @@ -441,7 +446,7 @@ public class MagicAudioManager { // Correct user selected audio devices if needed. if (userSelectedAudioDevice == AudioDevice.BLUETOOTH - && bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE) { + && bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_UNAVAILABLE) { userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; } if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE && hasWiredHeadset) { @@ -455,21 +460,21 @@ public class MagicAudioManager { // Need to start Bluetooth if it is available and user either selected it explicitly or // user did not select any output device. boolean needBluetoothAudioStart = - bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE + bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE && (userSelectedAudioDevice == AudioDevice.NONE || userSelectedAudioDevice == AudioDevice.BLUETOOTH); // Need to stop Bluetooth audio if user selected different device and // Bluetooth SCO connection is established or in the process. boolean needBluetoothAudioStop = - (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING) + (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING) && (userSelectedAudioDevice != AudioDevice.NONE && userSelectedAudioDevice != AudioDevice.BLUETOOTH); - if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { + if (bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED) { Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " + "stop=" + needBluetoothAudioStop + ", " + "BT state=" + bluetoothManager.getState()); @@ -494,7 +499,7 @@ public class MagicAudioManager { // Update selected audio device. AudioDevice newCurrentAudioDevice; - if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { + if (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED) { // If a Bluetooth is connected, then it should be used as output audio // device. Note that it is not sufficient that a headset is available; // an active SCO channel must also be up and running. diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcBluetoothManager.java similarity index 91% rename from app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java rename to app/src/main/java/com/nextcloud/talk/webrtc/WebRtcBluetoothManager.java index 2d067298b..0509efc83 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcBluetoothManager.java @@ -2,6 +2,8 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Tim Krüger + * Copyright (C) 2022 Tim Krüger * Copyright (C) 2017 Mario Danic * * This program is free software: you can redistribute it and/or modify @@ -31,6 +33,7 @@ package com.nextcloud.talk.webrtc; +import android.Manifest; import android.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -47,20 +50,23 @@ import android.os.Handler; import android.os.Looper; import android.os.Process; import android.util.Log; + import org.webrtc.ThreadUtils; import java.util.List; import java.util.Set; -public class MagicBluetoothManager { - private static final String TAG = "MagicBluetoothManager"; +import androidx.core.app.ActivityCompat; + +public class WebRtcBluetoothManager { + private static final String TAG = WebRtcBluetoothManager.class.getCanonicalName(); // Timeout interval for starting or stopping audio to a Bluetooth SCO device. private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; // Maximum number of SCO connection attempts. private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; private final Context apprtcContext; - private final MagicAudioManager apprtcAudioManager; + private final WebRtcAudioManger webRtcAudioManager; private final AudioManager audioManager; private final Handler handler; private final BluetoothProfile.ServiceListener bluetoothServiceListener; @@ -73,18 +79,14 @@ public class MagicBluetoothManager { // Runs when the Bluetooth timeout expires. We use that timeout after calling // startScoAudio() or stopScoAudio() because we're not guaranteed to get a // callback after those calls. - private final Runnable bluetoothTimeoutRunnable = new Runnable() { - @Override - public void run() { - bluetoothTimeout(); - } - }; + private final Runnable bluetoothTimeoutRunnable = this::bluetoothTimeout; + private boolean started = false; - protected MagicBluetoothManager(Context context, MagicAudioManager audioManager) { + protected WebRtcBluetoothManager(Context context, WebRtcAudioManger audioManager) { Log.d(TAG, "ctor"); ThreadUtils.checkIsOnMainThread(); apprtcContext = context; - apprtcAudioManager = audioManager; + webRtcAudioManager = audioManager; this.audioManager = getAudioManager(context); bluetoothState = State.UNINITIALIZED; bluetoothServiceListener = new BluetoothServiceListener(); @@ -95,8 +97,8 @@ public class MagicBluetoothManager { /** * Construction. */ - static MagicBluetoothManager create(Context context, MagicAudioManager audioManager) { - return new MagicBluetoothManager(context, audioManager); + static WebRtcBluetoothManager create(Context context, WebRtcAudioManger audioManager) { + return new WebRtcBluetoothManager(context, audioManager); } /** @@ -122,11 +124,11 @@ public class MagicBluetoothManager { * Note that the MagicAudioManager is also involved in driving this state * change. */ + @SuppressLint("MissingPermission") public void start() { ThreadUtils.checkIsOnMainThread(); Log.d(TAG, "start"); - if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) { - Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission"); + if(hasNoBluetoothPermission()){ return; } if (bluetoothState != State.UNINITIALIZED) { @@ -166,6 +168,7 @@ public class MagicBluetoothManager { + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); Log.d(TAG, "Bluetooth proxy for headset profile has started"); bluetoothState = State.HEADSET_UNAVAILABLE; + started = true; Log.d(TAG, "start done: BT state=" + bluetoothState); } @@ -262,8 +265,12 @@ public class MagicBluetoothManager { * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected * device if available. */ + @SuppressLint("MissingPermission") public void updateDevice() { - if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { + boolean hasNoBluetoothPermissions = hasNoBluetoothPermission(); + if (hasNoBluetoothPermissions || + bluetoothState == State.UNINITIALIZED || + bluetoothHeadset == null) { return; } Log.d(TAG, "updateDevice"); @@ -307,16 +314,28 @@ public class MagicBluetoothManager { return bluetoothAdapter.getProfileProxy(context, listener, profile); } - protected boolean hasPermission(Context context, String permission) { - return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid()) - == PackageManager.PERMISSION_GRANTED; + private boolean hasNoBluetoothPermission() { + String permission; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permission = Manifest.permission.BLUETOOTH_CONNECT; + } else { + permission = Manifest.permission.BLUETOOTH; + } + + boolean hasPermission = + ActivityCompat.checkSelfPermission(apprtcContext, permission) == PackageManager.PERMISSION_GRANTED; + if(!hasPermission) { + Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks \"" + permission + "\" permission"); + } + return !hasPermission; } /** * Logs the state of the local Bluetooth adapter. */ - @SuppressLint("HardwareIds") - protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { + @SuppressLint({"HardwareIds", "MissingPermission"}) + private void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { Log.d(TAG, "BluetoothAdapter: " + "enabled=" + localAdapter.isEnabled() + ", " + "state=" + stateToString(localAdapter.getState()) + ", " @@ -337,7 +356,7 @@ public class MagicBluetoothManager { private void updateAudioDeviceState() { ThreadUtils.checkIsOnMainThread(); Log.d(TAG, "updateAudioDeviceState"); - apprtcAudioManager.updateAudioDeviceState(); + webRtcAudioManager.updateAudioDeviceState(); } /** @@ -362,9 +381,13 @@ public class MagicBluetoothManager { * Called when start of the BT SCO channel takes too long time. Usually * happens when the BT device has been turned on during an ongoing call. */ + @SuppressLint("MissingPermission") private void bluetoothTimeout() { ThreadUtils.checkIsOnMainThread(); - if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { + boolean hasNoBluetoothPermissions = hasNoBluetoothPermission(); + if (hasNoBluetoothPermissions || + bluetoothState == State.UNINITIALIZED || + bluetoothHeadset == null) { return; } Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " @@ -435,6 +458,10 @@ public class MagicBluetoothManager { } } + public boolean started() { + return started; + } + // Bluetooth connection state. public enum State { // Bluetooth is not available; no adapter or Bluetooth is off.