diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml
index d03cf083a0..796a4b8b7a 100644
--- a/amethyst/src/main/AndroidManifest.xml
+++ b/amethyst/src/main/AndroidManifest.xml
@@ -14,8 +14,9 @@
-
+
+
@@ -40,10 +41,13 @@
+
+
+
@@ -243,7 +247,7 @@
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallAudioManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallAudioManager.kt
index 40c81c0487..0879145506 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallAudioManager.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallAudioManager.kt
@@ -20,10 +20,12 @@
*/
package com.vitorpamplona.amethyst.service.call
+import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
@@ -39,6 +41,7 @@ import android.os.PowerManager
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
+import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -63,6 +66,14 @@ class CallAudioManager(
private val _audioRoute = MutableStateFlow(AudioRoute.EARPIECE)
val audioRoute: StateFlow = _audioRoute.asStateFlow()
+ private fun hasBluetoothPermission(): Boolean =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) ==
+ PackageManager.PERMISSION_GRANTED
+ } else {
+ true
+ }
+
private val _isBluetoothAvailable = MutableStateFlow(false)
val isBluetoothAvailable: StateFlow = _isBluetoothAvailable.asStateFlow()
@@ -115,13 +126,18 @@ class CallAudioManager(
previousAudioMode = audioManager.mode
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
- _isBluetoothAvailable.value = hasBluetoothDevice()
- if (_isBluetoothAvailable.value) {
- startBluetoothSco()
+ if (hasBluetoothPermission()) {
+ _isBluetoothAvailable.value = hasBluetoothDevice()
+ if (_isBluetoothAvailable.value) {
+ startBluetoothSco()
+ } else {
+ routeToEarpiece()
+ }
+ registerBluetoothScoReceiver()
} else {
+ _isBluetoothAvailable.value = false
routeToEarpiece()
}
- registerBluetoothScoReceiver()
}
fun restoreAudioMode() {
@@ -220,6 +236,7 @@ class CallAudioManager(
}
private fun routeToBluetooth() {
+ if (!hasBluetoothPermission()) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val btDevice =
audioManager
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt
index e8b35e8163..c841892db1 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt
@@ -461,10 +461,12 @@ class CallController(
private fun startForegroundService() {
try {
val peerName = callManager.currentPeerPubKey() ?: ""
+ val isVideo = _isVideoEnabled.value
val intent =
Intent(context, CallForegroundService::class.java).apply {
action = CallForegroundService.ACTION_START
putExtra(CallForegroundService.EXTRA_PEER_NAME, peerName)
+ putExtra(CallForegroundService.EXTRA_IS_VIDEO, isVideo)
}
context.startForegroundService(intent)
} catch (e: Exception) {
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt
index 607e48155f..1b0a00a492 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt
@@ -45,6 +45,7 @@ class CallForegroundService : Service() {
const val ACTION_START = "com.vitorpamplona.amethyst.CALL_START"
const val ACTION_STOP = "com.vitorpamplona.amethyst.CALL_STOP"
const val EXTRA_PEER_NAME = "peer_name"
+ const val EXTRA_IS_VIDEO = "is_video"
}
override fun onBind(intent: Intent?): IBinder? = null
@@ -62,14 +63,25 @@ class CallForegroundService : Service() {
when (intent?.action) {
ACTION_START -> {
val peerName = intent.getStringExtra(EXTRA_PEER_NAME) ?: "Unknown"
+ val isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false)
val notification = buildNotification(peerName)
val hasAudioPermission =
ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
+ val hasCameraPermission =
+ ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
+ PackageManager.PERMISSION_GRANTED
try {
val fgsType =
- if (hasAudioPermission && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ var type = 0
+ if (hasAudioPermission) {
+ type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
+ }
+ if (isVideo && hasCameraPermission) {
+ type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
+ }
+ type
} else {
0
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallPermissions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallPermissions.kt
index 841caf9469..50717b1005 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallPermissions.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallPermissions.kt
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.call
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
+import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
@@ -43,30 +44,42 @@ fun hasCallPermissions(
return true
}
+fun buildCallPermissions(isVideo: Boolean): Array {
+ val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO)
+ if (isVideo) {
+ permissions.add(Manifest.permission.CAMERA)
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
+ }
+ return permissions.toTypedArray()
+}
+
@Composable
fun rememberCallWithPermission(
context: Context,
isVideo: Boolean = false,
onCall: () -> Unit,
): () -> Unit {
- val permissions =
- if (isVideo) {
- arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
- } else {
- arrayOf(Manifest.permission.RECORD_AUDIO)
- }
+ val permissions = remember(isVideo) { buildCallPermissions(isVideo) }
val launcher =
rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
) { results ->
- val allGranted = results.values.all { it }
- if (allGranted) onCall()
+ // Bluetooth is optional — proceed if core permissions are granted
+ if (hasCallPermissions(context, isVideo)) onCall()
}
return remember(onCall, isVideo) {
{
if (hasCallPermissions(context, isVideo)) {
+ // Core permissions granted; still request BT if missing
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+ !hasPermission(context, Manifest.permission.BLUETOOTH_CONNECT)
+ ) {
+ launcher.launch(arrayOf(Manifest.permission.BLUETOOTH_CONNECT))
+ }
onCall()
} else {
launcher.launch(permissions)