feat: disable outgoing video when phone is near ear during calls

Uses the proximity sensor to detect when the phone is held to the ear
and automatically pauses the camera/video track. Video resumes when the
phone is moved away, but only if the user had video enabled.

https://claude.ai/code/session_014espsysob7MrE8X8e75jFX
This commit is contained in:
Claude
2026-04-02 18:35:59 +00:00
parent 2cb88185e2
commit ea96dc9fb9
2 changed files with 56 additions and 0 deletions
@@ -24,6 +24,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.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioDeviceInfo import android.media.AudioDeviceInfo
import android.media.AudioManager import android.media.AudioManager
@@ -62,6 +66,25 @@ class CallAudioManager(
private val _isBluetoothAvailable = MutableStateFlow(false) private val _isBluetoothAvailable = MutableStateFlow(false)
val isBluetoothAvailable: StateFlow<Boolean> = _isBluetoothAvailable.asStateFlow() val isBluetoothAvailable: StateFlow<Boolean> = _isBluetoothAvailable.asStateFlow()
// Proximity sensor — detects when the phone is held near the ear
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
private val proximitySensor: Sensor? = sensorManager?.getDefaultSensor(Sensor.TYPE_PROXIMITY)
private val _isNearEar = MutableStateFlow(false)
val isNearEar: StateFlow<Boolean> = _isNearEar.asStateFlow()
private val proximityListener =
object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
val maxRange = event.sensor.maximumRange
_isNearEar.value = event.values[0] < maxRange
}
override fun onAccuracyChanged(
sensor: Sensor?,
accuracy: Int,
) {}
}
fun startRinging() { fun startRinging() {
startRingtone() startRingtone()
startVibration() startVibration()
@@ -137,11 +160,24 @@ class CallAudioManager(
"amethyst:call_proximity", "amethyst:call_proximity",
) )
proximityWakeLock?.acquire(60 * 60 * 1000L) proximityWakeLock?.acquire(60 * 60 * 1000L)
registerProximitySensor()
} }
fun releaseProximityWakeLock() { fun releaseProximityWakeLock() {
proximityWakeLock?.let { if (it.isHeld) it.release() } proximityWakeLock?.let { if (it.isHeld) it.release() }
proximityWakeLock = null proximityWakeLock = null
unregisterProximitySensor()
}
private fun registerProximitySensor() {
proximitySensor?.let {
sensorManager?.registerListener(proximityListener, it, SensorManager.SENSOR_DELAY_NORMAL)
}
}
private fun unregisterProximitySensor() {
sensorManager?.unregisterListener(proximityListener)
_isNearEar.value = false
} }
fun release() { fun release() {
@@ -93,6 +93,9 @@ class CallController(
val audioRoute: StateFlow<AudioRoute> = audioManager.audioRoute val audioRoute: StateFlow<AudioRoute> = audioManager.audioRoute
val isBluetoothAvailable: StateFlow<Boolean> = audioManager.isBluetoothAvailable val isBluetoothAvailable: StateFlow<Boolean> = audioManager.isBluetoothAvailable
// Tracks whether video is paused because the phone is near the ear
private var videoPausedByProximity = false
init { init {
callManager.onRenegotiationOfferReceived = { event -> onRenegotiationOfferReceived(event) } callManager.onRenegotiationOfferReceived = { event -> onRenegotiationOfferReceived(event) }
@@ -126,6 +129,22 @@ class CallController(
} }
} }
} }
// Pause outgoing video when the phone is held near the ear
scope.launch {
audioManager.isNearEar.collect { nearEar ->
val session = webRtcSession ?: return@collect
if (nearEar && _isVideoEnabled.value && !videoPausedByProximity) {
videoPausedByProximity = true
session.setVideoEnabled(false)
session.stopCamera()
} else if (!nearEar && videoPausedByProximity) {
videoPausedByProximity = false
session.setVideoEnabled(true)
session.startCamera()
}
}
}
} }
fun initiateCall( fun initiateCall(
@@ -323,6 +342,7 @@ class CallController(
_isAudioMuted.value = false _isAudioMuted.value = false
_isVideoEnabled.value = false _isVideoEnabled.value = false
_isRemoteVideoActive.value = false _isRemoteVideoActive.value = false
videoPausedByProximity = false
webRtcSession?.dispose() webRtcSession?.dispose()
webRtcSession = null webRtcSession = null
remoteDescriptionSet.set(false) remoteDescriptionSet.set(false)