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.Intent
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.AudioDeviceInfo
import android.media.AudioManager
@@ -62,6 +66,25 @@ class CallAudioManager(
private val _isBluetoothAvailable = MutableStateFlow(false)
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() {
startRingtone()
startVibration()
@@ -137,11 +160,24 @@ class CallAudioManager(
"amethyst:call_proximity",
)
proximityWakeLock?.acquire(60 * 60 * 1000L)
registerProximitySensor()
}
fun releaseProximityWakeLock() {
proximityWakeLock?.let { if (it.isHeld) it.release() }
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() {
@@ -93,6 +93,9 @@ class CallController(
val audioRoute: StateFlow<AudioRoute> = audioManager.audioRoute
val isBluetoothAvailable: StateFlow<Boolean> = audioManager.isBluetoothAvailable
// Tracks whether video is paused because the phone is near the ear
private var videoPausedByProximity = false
init {
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(
@@ -323,6 +342,7 @@ class CallController(
_isAudioMuted.value = false
_isVideoEnabled.value = false
_isRemoteVideoActive.value = false
videoPausedByProximity = false
webRtcSession?.dispose()
webRtcSession = null
remoteDescriptionSet.set(false)