fix: add missing call permissions for all supported Android versions
- Add MODIFY_AUDIO_SETTINGS for audio routing (speaker, earpiece, BT SCO) - Add BLUETOOTH permission for API 26-30 (maxSdkVersion=30) - Request BLUETOOTH_CONNECT at runtime on API 31+ for BT audio - Add FOREGROUND_SERVICE_CAMERA for video calls on API 34+ - Update foreground service type to microphone|camera for video calls - Guard Bluetooth operations with permission check in CallAudioManager - Add uses-feature for microphone with required=false https://claude.ai/code/session_01LR8NmFGdMoDTVcfKjL7HWR
This commit is contained in:
@@ -14,8 +14,9 @@
|
||||
</queries>
|
||||
|
||||
|
||||
<!-- Doesn't require a camera -->
|
||||
<!-- Doesn't require a camera or microphone -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="false" />
|
||||
|
||||
<!-- To connect with relays -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
@@ -40,10 +41,13 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
|
||||
<!-- Phone calls -->
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<!-- Keeps screen on while playing videos -->
|
||||
@@ -243,7 +247,7 @@
|
||||
|
||||
<service
|
||||
android:name=".service.call.CallForegroundService"
|
||||
android:foregroundServiceType="microphone"
|
||||
android:foregroundServiceType="microphone|camera"
|
||||
android:stopWithTask="true"
|
||||
android:exported="false" />
|
||||
|
||||
|
||||
@@ -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> = _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<Boolean> = _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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+14
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user