diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 9cdf2e20..c4aa63c7 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -10,8 +10,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.database.ContentObserver import android.media.* +import android.media.AudioManager.MODE_NORMAL import android.media.AudioManager.RINGER_MODE_SILENT import android.net.* import android.net.wifi.WifiManager @@ -28,9 +30,9 @@ import android.widget.RemoteViews import android.widget.Toast import androidx.annotation.ColorRes import androidx.annotation.Keep -import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData @@ -41,10 +43,10 @@ import java.io.File import java.net.InetAddress import java.nio.charset.StandardCharsets import java.util.* -import kotlin.collections.ArrayList import kotlin.concurrent.schedule import kotlin.math.roundToInt + class BaresipService: Service() { internal lateinit var intent: Intent @@ -244,19 +246,22 @@ class BaresipService: Service() { when (state) { BluetoothHeadset.STATE_CONNECTED -> { Log.d(TAG, "Bluetooth headset is connected") - if (audioFocusRequest != null) - // Without delay, SCO_AUDIO_STATE_CONNECTING -> - // SCO_AUDIO_STATE_DISCONNECTED - Timer("Sco", false).schedule(1000) { - Log.d(TAG, "Starting Bluetooth SCO") - am.startBluetoothSco() - } + if (audioFocusRequest != null) { + //Handler(Looper.getMainLooper()).postDelayed({ + //am.startBluetoothSco() + startBluetoothSco(am, 1000L) + + //}, 1000) + } + //Timer("Sco", false).schedule(1000) { + // startBluetoothSco(am) + // } } BluetoothHeadset.STATE_DISCONNECTED -> { Log.d(TAG, "Bluetooth headset is disconnected") - if (am.isBluetoothScoOn) { + if (isBluetoothScoOn(am)) { Log.d(TAG, "Stopping Bluetooth SCO") - am.stopBluetoothSco() + stopBluetoothSco(am) } } @@ -271,12 +276,10 @@ class BaresipService: Service() { } BluetoothHeadset.STATE_AUDIO_DISCONNECTED -> { Log.d(TAG, "Bluetooth headset audio is disconnected") - if (am.isBluetoothScoOn) { - Log.d(TAG, "Stopping Bluetooth SCO") - am.stopBluetoothSco() + if (isBluetoothScoOn(am)) { + stopBluetoothSco(am) } } - } } AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> { @@ -468,7 +471,7 @@ class BaresipService: Service() { val vidMode = intent.getIntExtra("video", Api.VIDMODE_OFF) stopRinging() stopMediaPlayer() - requestAudioFocus(am, AudioAttributes.CONTENT_TYPE_SPEECH) + //requestAudioFocus(applicationContext, am, AudioAttributes.CONTENT_TYPE_SPEECH) setCallVolume() proximitySensing(true) Api.ua_answer(uap, callp, vidMode) @@ -886,9 +889,8 @@ class BaresipService: Service() { call.remove() if (Call.calls().size == 0) { resetCallVolume() - am.mode = AudioManager.MODE_NORMAL - am.stopBluetoothSco() abandonAudioFocus(am) + Utils.clearCommunicationDevice(am) proximitySensing(false) } val missed = call.startTime == null && call.dir == "in" && !call.rejected @@ -1188,11 +1190,12 @@ class BaresipService: Service() { } private fun startRinging() { - requestAudioFocus(am, AudioAttributes.CONTENT_TYPE_MUSIC) - am.mode = AudioManager.MODE_RINGTONE + requestAudioFocus(applicationContext, am, AudioAttributes.CONTENT_TYPE_MUSIC) if (rt == null) { - val rtUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext, - RingtoneManager.TYPE_RINGTONE) + val rtUri = RingtoneManager.getActualDefaultRingtoneUri( + applicationContext, + RingtoneManager.TYPE_RINGTONE + ) rt = RingtoneManager.getRingtone(applicationContext, rtUri) } if (VERSION.SDK_INT >= 28) { @@ -1251,8 +1254,6 @@ class BaresipService: Service() { } private fun stopRinging() { - if (am.mode == AudioManager.MODE_RINGTONE) - abandonAudioFocus(am) if (VERSION.SDK_INT < 28 && rtTimer != null) { rtTimer!!.cancel() rtTimer = null @@ -1307,13 +1308,10 @@ class BaresipService: Service() { if (callVolume != 0 && origVolume.isEmpty()) { for (streamType in listOf(AudioManager.STREAM_MUSIC, AudioManager.STREAM_VOICE_CALL)) { origVolume[streamType] = am.getStreamVolume(streamType) - am.setStreamVolume( - streamType, - (callVolume * 0.1 * am.getStreamMaxVolume(streamType)).roundToInt(), - 0 - ) - Log.d(TAG, "Orig/new $streamType volume is " + - "${origVolume[streamType]}/${am.getStreamVolume(streamType)}") + val maxVolume = am.getStreamMaxVolume(streamType) + am.setStreamVolume(streamType, (callVolume * 0.1 * maxVolume).roundToInt(), 0) + Log.d(TAG, "Orig/new/max $streamType volume is " + + "${origVolume[streamType]}/${am.getStreamVolume(streamType)}/$maxVolume") } } } @@ -1490,11 +1488,8 @@ class BaresipService: Service() { if (!isServiceClean) { this.unregisterReceiver(bluetoothReceiver) this.unregisterReceiver(hotSpotReceiver) - if (am.isBluetoothScoOn) - am.stopBluetoothSco() - am.mode = AudioManager.MODE_NORMAL - abandonAudioFocus(am) stopRinging() + abandonAudioFocus(am) stopMediaPlayer() uas.clear() callHistory.clear() @@ -1552,13 +1547,16 @@ class BaresipService: Service() { private var audioFocusRequest: AudioFocusRequestCompat? = 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") 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 - else + } else { abandonAudioFocus(am) + } } val attributes = AudioAttributesCompat.Builder() .setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION) @@ -1571,12 +1569,13 @@ class BaresipService: Service() { if (AudioManagerCompat.requestAudioFocus(am, audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.d(TAG, "Audio focus granted") isAudioFocused = true - if (isBluetoothHeadsetConnected() && !am.isBluetoothScoOn) { + if (isBluetoothHeadsetConnected(ctx) /* && !am.isBluetoothScoOn */) { 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 { Log.i(TAG, "Audio focus denied") audioFocusRequest = null @@ -1587,23 +1586,62 @@ class BaresipService: Service() { private fun abandonAudioFocus(am: AudioManager) { if (audioFocusRequest != null) { + Log.d(TAG, "Abandoning audio focus") AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) audioFocusRequest = null isAudioFocused = false - Log.d(TAG, "Audio focus abandoned") + stopBluetoothSco(am) + Utils.clearCommunicationDevice(am) + am.mode = MODE_NORMAL } } @SuppressLint("MissingPermission") - private fun isBluetoothHeadsetConnected(): Boolean { - return if (VERSION.SDK_INT < 31) + private fun isBluetoothHeadsetConnected(ctx: Context): Boolean { + return if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.BLUETOOTH_CONNECT) == + PackageManager.PERMISSION_GRANTED) btAdapter != null && btAdapter!!.isEnabled && btAdapter!!.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothAdapter.STATE_CONNECTED else - // getProfileConnectionState requires asking for BLUETOOTH_CONNECT permission 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 { diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index b8f6f61c..73375f0d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -1,6 +1,7 @@ package com.tutpro.baresip import android.Manifest +import android.Manifest.permission.* import android.annotation.SuppressLint import android.app.Activity import android.app.AlertDialog @@ -80,6 +81,7 @@ class MainActivity : AppCompatActivity() { private var speakerIcon: MenuItem? = null private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var requestPermissionLauncher: ActivityResultLauncher + private lateinit var requestPermissionsLauncher: ActivityResultLauncher> private lateinit var accountsRequest: ActivityResultLauncher private lateinit var chatRequests: ActivityResultLauncher private lateinit var configRequest: ActivityResultLauncher @@ -676,6 +678,22 @@ class MainActivity : AppCompatActivity() { } } + requestPermissionsLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { isGranted: Map -> + 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()) { AppCompatDelegate.setDefaultNightMode(Preferences(applicationContext).displayTheme) delegate.applyDayNight() @@ -685,11 +703,35 @@ class MainActivity : AppCompatActivity() { override fun onStart() { super.onStart() - Log.d(TAG, "Main onStart") + Log.e(TAG, "Main onStart") - if (!Utils.checkPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO))) - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), - MIC_PERMISSION_REQUEST_CODE) + if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO, BLUETOOTH_CONNECT))) + if (ActivityCompat.shouldShowRequestPermissionRationale(this, RECORD_AUDIO) || + 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") if (action != null) { @@ -1817,7 +1859,7 @@ class MainActivity : AppCompatActivity() { hangupButton.visibility = View.VISIBLE hangupButton.isEnabled = true 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( applicationContext, R.string.audio_focus_denied, @@ -1856,7 +1898,7 @@ class MainActivity : AppCompatActivity() { } } am.addOnModeChangedListener(mainExecutor, audioModeChangedListener!!) - if (!BaresipService.requestAudioFocus(am, AudioAttributes.CONTENT_TYPE_SPEECH)) + if (!BaresipService.requestAudioFocus(this, am, AudioAttributes.CONTENT_TYPE_SPEECH)) Toast.makeText( applicationContext, R.string.audio_focus_denied, diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 338e063e..30265369 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -921,7 +921,7 @@ object Utils { } if (current != speakerDevice.type) { if (speakerDevice.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { - am.clearCommunicationDevice() + clearCommunicationDevice(am) Log.d(TAG, "Setting com device to TYPE_BUILTIN_EARPIECE") if (!am.setCommunicationDevice(speakerDevice)) 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 + } + } + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8a0dd21..9c374c89 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -508,8 +508,9 @@ You are not able to use this application without \"Notifications\" permission. - You are not able to make or answer calls without \"Microphone\" - permission. + baresip needs \"Microphone\" permission for voice calls. + baresip is not able to detect Bluetooth connectivity without + \"Nearby devices\" permission. Grant \"Camera\" permission to make or answer video calls. You are not able create backup without \"Storage\" permission. You are not able restore backup without \"Storage\" permission. @@ -519,4 +520,6 @@ Show Password No network connection! Audio focus denied! + baresip needs \"Microphone\" permission for calls and + \"Nearby devices\" permission for Bluetooth detection.