Started work on Bluetooth improvements for API leve 31+

This commit is contained in:
Juha Heinanen
2023-02-20 20:29:15 +02:00
parent 0c8cca2076
commit 7b1f11e1e2
4 changed files with 158 additions and 55 deletions
@@ -10,8 +10,10 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager
import android.database.ContentObserver import android.database.ContentObserver
import android.media.* import android.media.*
import android.media.AudioManager.MODE_NORMAL
import android.media.AudioManager.RINGER_MODE_SILENT import android.media.AudioManager.RINGER_MODE_SILENT
import android.net.* import android.net.*
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
@@ -28,9 +30,9 @@ import android.widget.RemoteViews
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -41,10 +43,10 @@ import java.io.File
import java.net.InetAddress import java.net.InetAddress
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.* import java.util.*
import kotlin.collections.ArrayList
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
class BaresipService: Service() { class BaresipService: Service() {
internal lateinit var intent: Intent internal lateinit var intent: Intent
@@ -244,19 +246,22 @@ class BaresipService: Service() {
when (state) { when (state) {
BluetoothHeadset.STATE_CONNECTED -> { BluetoothHeadset.STATE_CONNECTED -> {
Log.d(TAG, "Bluetooth headset is connected") Log.d(TAG, "Bluetooth headset is connected")
if (audioFocusRequest != null) if (audioFocusRequest != null) {
// Without delay, SCO_AUDIO_STATE_CONNECTING -> //Handler(Looper.getMainLooper()).postDelayed({
// SCO_AUDIO_STATE_DISCONNECTED //am.startBluetoothSco()
Timer("Sco", false).schedule(1000) { startBluetoothSco(am, 1000L)
Log.d(TAG, "Starting Bluetooth SCO")
am.startBluetoothSco() //}, 1000)
} }
//Timer("Sco", false).schedule(1000) {
// startBluetoothSco(am)
// }
} }
BluetoothHeadset.STATE_DISCONNECTED -> { BluetoothHeadset.STATE_DISCONNECTED -> {
Log.d(TAG, "Bluetooth headset is disconnected") Log.d(TAG, "Bluetooth headset is disconnected")
if (am.isBluetoothScoOn) { if (isBluetoothScoOn(am)) {
Log.d(TAG, "Stopping Bluetooth SCO") Log.d(TAG, "Stopping Bluetooth SCO")
am.stopBluetoothSco() stopBluetoothSco(am)
} }
} }
@@ -271,12 +276,10 @@ class BaresipService: Service() {
} }
BluetoothHeadset.STATE_AUDIO_DISCONNECTED -> { BluetoothHeadset.STATE_AUDIO_DISCONNECTED -> {
Log.d(TAG, "Bluetooth headset audio is disconnected") Log.d(TAG, "Bluetooth headset audio is disconnected")
if (am.isBluetoothScoOn) { if (isBluetoothScoOn(am)) {
Log.d(TAG, "Stopping Bluetooth SCO") stopBluetoothSco(am)
am.stopBluetoothSco()
} }
} }
} }
} }
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> { AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
@@ -468,7 +471,7 @@ class BaresipService: Service() {
val vidMode = intent.getIntExtra("video", Api.VIDMODE_OFF) val vidMode = intent.getIntExtra("video", Api.VIDMODE_OFF)
stopRinging() stopRinging()
stopMediaPlayer() stopMediaPlayer()
requestAudioFocus(am, AudioAttributes.CONTENT_TYPE_SPEECH) //requestAudioFocus(applicationContext, am, AudioAttributes.CONTENT_TYPE_SPEECH)
setCallVolume() setCallVolume()
proximitySensing(true) proximitySensing(true)
Api.ua_answer(uap, callp, vidMode) Api.ua_answer(uap, callp, vidMode)
@@ -886,9 +889,8 @@ class BaresipService: Service() {
call.remove() call.remove()
if (Call.calls().size == 0) { if (Call.calls().size == 0) {
resetCallVolume() resetCallVolume()
am.mode = AudioManager.MODE_NORMAL
am.stopBluetoothSco()
abandonAudioFocus(am) abandonAudioFocus(am)
Utils.clearCommunicationDevice(am)
proximitySensing(false) proximitySensing(false)
} }
val missed = call.startTime == null && call.dir == "in" && !call.rejected val missed = call.startTime == null && call.dir == "in" && !call.rejected
@@ -1188,11 +1190,12 @@ class BaresipService: Service() {
} }
private fun startRinging() { private fun startRinging() {
requestAudioFocus(am, AudioAttributes.CONTENT_TYPE_MUSIC) requestAudioFocus(applicationContext, am, AudioAttributes.CONTENT_TYPE_MUSIC)
am.mode = AudioManager.MODE_RINGTONE
if (rt == null) { if (rt == null) {
val rtUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext, val rtUri = RingtoneManager.getActualDefaultRingtoneUri(
RingtoneManager.TYPE_RINGTONE) applicationContext,
RingtoneManager.TYPE_RINGTONE
)
rt = RingtoneManager.getRingtone(applicationContext, rtUri) rt = RingtoneManager.getRingtone(applicationContext, rtUri)
} }
if (VERSION.SDK_INT >= 28) { if (VERSION.SDK_INT >= 28) {
@@ -1251,8 +1254,6 @@ class BaresipService: Service() {
} }
private fun stopRinging() { private fun stopRinging() {
if (am.mode == AudioManager.MODE_RINGTONE)
abandonAudioFocus(am)
if (VERSION.SDK_INT < 28 && rtTimer != null) { if (VERSION.SDK_INT < 28 && rtTimer != null) {
rtTimer!!.cancel() rtTimer!!.cancel()
rtTimer = null rtTimer = null
@@ -1307,13 +1308,10 @@ class BaresipService: Service() {
if (callVolume != 0 && origVolume.isEmpty()) { if (callVolume != 0 && origVolume.isEmpty()) {
for (streamType in listOf(AudioManager.STREAM_MUSIC, AudioManager.STREAM_VOICE_CALL)) { for (streamType in listOf(AudioManager.STREAM_MUSIC, AudioManager.STREAM_VOICE_CALL)) {
origVolume[streamType] = am.getStreamVolume(streamType) origVolume[streamType] = am.getStreamVolume(streamType)
am.setStreamVolume( val maxVolume = am.getStreamMaxVolume(streamType)
streamType, am.setStreamVolume(streamType, (callVolume * 0.1 * maxVolume).roundToInt(), 0)
(callVolume * 0.1 * am.getStreamMaxVolume(streamType)).roundToInt(), Log.d(TAG, "Orig/new/max $streamType volume is " +
0 "${origVolume[streamType]}/${am.getStreamVolume(streamType)}/$maxVolume")
)
Log.d(TAG, "Orig/new $streamType volume is " +
"${origVolume[streamType]}/${am.getStreamVolume(streamType)}")
} }
} }
} }
@@ -1490,11 +1488,8 @@ class BaresipService: Service() {
if (!isServiceClean) { if (!isServiceClean) {
this.unregisterReceiver(bluetoothReceiver) this.unregisterReceiver(bluetoothReceiver)
this.unregisterReceiver(hotSpotReceiver) this.unregisterReceiver(hotSpotReceiver)
if (am.isBluetoothScoOn)
am.stopBluetoothSco()
am.mode = AudioManager.MODE_NORMAL
abandonAudioFocus(am)
stopRinging() stopRinging()
abandonAudioFocus(am)
stopMediaPlayer() stopMediaPlayer()
uas.clear() uas.clear()
callHistory.clear() callHistory.clear()
@@ -1552,13 +1547,16 @@ class BaresipService: Service() {
private var audioFocusRequest: AudioFocusRequestCompat? = null private var audioFocusRequest: AudioFocusRequestCompat? = null
private var btAdapter: BluetoothAdapter? = null private var btAdapter: BluetoothAdapter? = null
fun requestAudioFocus(am: AudioManager, type: Int): Boolean { fun requestAudioFocus(ctx: Context, am: AudioManager, type: Int): Boolean {
Log.d(TAG, "Requesting audio focus of type $type") Log.d(TAG, "Requesting audio focus of type $type")
if (audioFocusRequest != null) { if (audioFocusRequest != null) {
if (audioFocusRequest!!.audioAttributesCompat.contentType == type) if (audioFocusRequest!!.audioAttributesCompat.contentType == type) {
Log.d(TAG, "Already focused")
am.mode = AudioManager.MODE_IN_COMMUNICATION
return true return true
else } else {
abandonAudioFocus(am) abandonAudioFocus(am)
}
} }
val attributes = AudioAttributesCompat.Builder() val attributes = AudioAttributesCompat.Builder()
.setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION) .setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
@@ -1571,12 +1569,13 @@ class BaresipService: Service() {
if (AudioManagerCompat.requestAudioFocus(am, audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { if (AudioManagerCompat.requestAudioFocus(am, audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "Audio focus granted") Log.d(TAG, "Audio focus granted")
isAudioFocused = true isAudioFocused = true
if (isBluetoothHeadsetConnected() && !am.isBluetoothScoOn) { if (isBluetoothHeadsetConnected(ctx) /* && !am.isBluetoothScoOn */) {
Log.d(TAG, "Starting Bluetooth SCO") Log.d(TAG, "Starting Bluetooth SCO")
am.startBluetoothSco() startBluetoothSco(am, 0L)
} }
if (type == AudioAttributes.CONTENT_TYPE_SPEECH) am.mode = AudioManager.MODE_IN_COMMUNICATION
am.mode = AudioManager.MODE_IN_COMMUNICATION //if (type == AudioAttributes.CONTENT_TYPE_SPEECH || isBluetoothHeadsetConnected(ctx))
//am.mode = AudioManager.MODE_IN_COMMUNICATION
} else { } else {
Log.i(TAG, "Audio focus denied") Log.i(TAG, "Audio focus denied")
audioFocusRequest = null audioFocusRequest = null
@@ -1587,23 +1586,62 @@ class BaresipService: Service() {
private fun abandonAudioFocus(am: AudioManager) { private fun abandonAudioFocus(am: AudioManager) {
if (audioFocusRequest != null) { if (audioFocusRequest != null) {
Log.d(TAG, "Abandoning audio focus")
AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!)
audioFocusRequest = null audioFocusRequest = null
isAudioFocused = false isAudioFocused = false
Log.d(TAG, "Audio focus abandoned") stopBluetoothSco(am)
Utils.clearCommunicationDevice(am)
am.mode = MODE_NORMAL
} }
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private fun isBluetoothHeadsetConnected(): Boolean { private fun isBluetoothHeadsetConnected(ctx: Context): Boolean {
return if (VERSION.SDK_INT < 31) return if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED)
btAdapter != null && btAdapter!!.isEnabled && btAdapter != null && btAdapter!!.isEnabled &&
btAdapter!!.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothAdapter.STATE_CONNECTED btAdapter!!.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothAdapter.STATE_CONNECTED
else else
// getProfileConnectionState requires asking for BLUETOOTH_CONNECT permission
true true
} }
private fun isBluetoothScoOn(am: AudioManager): Boolean {
return if (VERSION.SDK_INT < 31)
am.isBluetoothScoOn
else
if (am.communicationDevice != null)
am.communicationDevice!!.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
else
false
}
private fun startBluetoothSco(am: AudioManager, delay: Long) {
Log.d(TAG, "Starting Bluetooth SCO")
Handler(Looper.getMainLooper()).postDelayed({
if (VERSION.SDK_INT < 35)
am.startBluetoothSco()
else
Utils.setCommunicationDevice(am, AudioDeviceInfo.TYPE_BLUETOOTH_SCO)
if (!isBluetoothScoOn(am)) {
Log.d(TAG, "Trying to start Bluetooth SCO again")
Handler(Looper.getMainLooper()).postDelayed({
am.startBluetoothSco()
}, delay)
}
}, delay)
}
private fun stopBluetoothSco(am: AudioManager) {
Log.d(TAG, "Stopping Bluetooth SCO")
Handler(Looper.getMainLooper()).postDelayed({
if (VERSION.SDK_INT < 35)
am.stopBluetoothSco()
else
am.clearCommunicationDevice()
}, 100)
}
} }
init { init {
@@ -1,6 +1,7 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.Manifest import android.Manifest
import android.Manifest.permission.*
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
@@ -80,6 +81,7 @@ class MainActivity : AppCompatActivity() {
private var speakerIcon: MenuItem? = null private var speakerIcon: MenuItem? = null
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String> private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private lateinit var requestPermissionsLauncher: ActivityResultLauncher<Array<String>>
private lateinit var accountsRequest: ActivityResultLauncher<Intent> private lateinit var accountsRequest: ActivityResultLauncher<Intent>
private lateinit var chatRequests: ActivityResultLauncher<Intent> private lateinit var chatRequests: ActivityResultLauncher<Intent>
private lateinit var configRequest: ActivityResultLauncher<Intent> private lateinit var configRequest: ActivityResultLauncher<Intent>
@@ -676,6 +678,22 @@ class MainActivity : AppCompatActivity() {
} }
} }
requestPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { isGranted: Map<String, Boolean> ->
Log.e(TAG, "********** isGranted $isGranted")
if (firstRun && isGranted.isEmpty()) {
with(MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)) {
setTitle("Permissions")
setMessage(getString(R.string.mic_and_bluetooth))
setPositiveButton(getString(R.string.ok)) { dialog, _ ->
requestPermissionsLauncher.launch(arrayOf(RECORD_AUDIO, BLUETOOTH_CONNECT))
dialog.dismiss()
}
show()
}
}
}
if (Preferences(applicationContext).displayTheme != AppCompatDelegate.getDefaultNightMode()) { if (Preferences(applicationContext).displayTheme != AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.setDefaultNightMode(Preferences(applicationContext).displayTheme) AppCompatDelegate.setDefaultNightMode(Preferences(applicationContext).displayTheme)
delegate.applyDayNight() delegate.applyDayNight()
@@ -685,11 +703,35 @@ class MainActivity : AppCompatActivity() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
Log.d(TAG, "Main onStart") Log.e(TAG, "Main onStart")
if (!Utils.checkPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO))) if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO, BLUETOOTH_CONNECT)))
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), if (ActivityCompat.shouldShowRequestPermissionRationale(this, RECORD_AUDIO) ||
MIC_PERMISSION_REQUEST_CODE) ActivityCompat.shouldShowRequestPermissionRationale(this, BLUETOOTH_CONNECT)) {
layout.showSnackBar(
binding.root,
if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO)) &&
!Utils.checkPermissions(this, arrayOf(BLUETOOTH_CONNECT)))
getString(R.string.mic_and_bluetooth)
else if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO)))
getString(R.string.no_calls)
else
getString(R.string.no_bluetooth),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionsLauncher.launch(arrayOf(RECORD_AUDIO, BLUETOOTH_CONNECT))
}
} else {
requestPermissionsLauncher.launch(
arrayOf(
RECORD_AUDIO,
BLUETOOTH_CONNECT
)
)
}
else
Log.e(TAG, "********** all permissions granted")
val action = intent.getStringExtra("action") val action = intent.getStringExtra("action")
if (action != null) { if (action != null) {
@@ -1817,7 +1859,7 @@ class MainActivity : AppCompatActivity() {
hangupButton.visibility = View.VISIBLE hangupButton.visibility = View.VISIBLE
hangupButton.isEnabled = true hangupButton.isEnabled = true
if (Build.VERSION.SDK_INT < 31) { if (Build.VERSION.SDK_INT < 31) {
if (!BaresipService.requestAudioFocus(am, AudioAttributes.CONTENT_TYPE_SPEECH)) { if (!BaresipService.requestAudioFocus(this, am, AudioAttributes.CONTENT_TYPE_SPEECH)) {
Toast.makeText( Toast.makeText(
applicationContext, applicationContext,
R.string.audio_focus_denied, R.string.audio_focus_denied,
@@ -1856,7 +1898,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
am.addOnModeChangedListener(mainExecutor, audioModeChangedListener!!) am.addOnModeChangedListener(mainExecutor, audioModeChangedListener!!)
if (!BaresipService.requestAudioFocus(am, AudioAttributes.CONTENT_TYPE_SPEECH)) if (!BaresipService.requestAudioFocus(this, am, AudioAttributes.CONTENT_TYPE_SPEECH))
Toast.makeText( Toast.makeText(
applicationContext, applicationContext,
R.string.audio_focus_denied, R.string.audio_focus_denied,
@@ -921,7 +921,7 @@ object Utils {
} }
if (current != speakerDevice.type) { if (current != speakerDevice.type) {
if (speakerDevice.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { if (speakerDevice.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
am.clearCommunicationDevice() clearCommunicationDevice(am)
Log.d(TAG, "Setting com device to TYPE_BUILTIN_EARPIECE") Log.d(TAG, "Setting com device to TYPE_BUILTIN_EARPIECE")
if (!am.setCommunicationDevice(speakerDevice)) if (!am.setCommunicationDevice(speakerDevice))
Log.e(TAG, "Could not set com device") Log.e(TAG, "Could not set com device")
@@ -973,4 +973,24 @@ object Utils {
} }
} }
fun setCommunicationDevice(am: AudioManager, type: Int) {
val current = am.communicationDevice!!.type
Log.d(TAG, "Current com dev/mode $current/${am.mode}")
for (device in am.availableCommunicationDevices)
if (device.type == type) {
am.setCommunicationDevice(device)
break
}
Log.d(TAG, "New com dev/mode ${am.communicationDevice!!.type}/${am.mode}")
}
fun clearCommunicationDevice(am: AudioManager) {
if (Build.VERSION.SDK_INT >= 31) {
am.clearCommunicationDevice()
} else {
if (am.isSpeakerphoneOn)
am.isSpeakerphoneOn = false
}
}
} }
+5 -2
View File
@@ -508,8 +508,9 @@
</string> </string>
<string name="no_notifications">You are not able to use this application without \"Notifications\" <string name="no_notifications">You are not able to use this application without \"Notifications\"
permission.</string> permission.</string>
<string name="no_calls">You are not able to make or answer calls without \"Microphone\" <string name="no_calls">baresip needs \"Microphone\" permission for voice calls.</string>
permission.</string> <string name="no_bluetooth">baresip is not able to detect Bluetooth connectivity without
\"Nearby devices\" permission.</string>
<string name="no_video_calls">Grant \"Camera\" permission to make or answer video calls.</string> <string name="no_video_calls">Grant \"Camera\" permission to make or answer video calls.</string>
<string name="no_backup">You are not able create backup without \"Storage\" permission.</string> <string name="no_backup">You are not able create backup without \"Storage\" permission.</string>
<string name="no_restore">You are not able restore backup without \"Storage\" permission.</string> <string name="no_restore">You are not able restore backup without \"Storage\" permission.</string>
@@ -519,4 +520,6 @@
<string name="show_password">Show Password</string> <string name="show_password">Show Password</string>
<string name="no_network">No network connection!</string> <string name="no_network">No network connection!</string>
<string name="audio_focus_denied">Audio focus denied!</string> <string name="audio_focus_denied">Audio focus denied!</string>
<string name="mic_and_bluetooth">baresip needs \"Microphone\" permission for calls and
\"Nearby devices\" permission for Bluetooth detection.</string>
</resources> </resources>