From aa794102c3b04cc3b94307542cc8d8cf69f051cd Mon Sep 17 00:00:00 2001 From: sravan Date: Thu, 29 Oct 2026 22:10:17 +0530 Subject: [PATCH] android(call): volume key silences the ring like a real phone The incoming ring is driven by our own Ringer (we aren't a telecom ConnectionService yet), so the OS doesn't route volume keys to silence it. Capture them two ways that together cover every state: - Foreground/locked: IncomingCallActivity.dispatchKeyEvent consumes VOLUME_UP/DOWN/MUTE and silences. - Unlocked/backgrounded heads-up (no activity): a MediaSession with a remote VolumeProvider so the framework delivers volume presses to us. Silence stops audio + vibration but keeps the call ringing visually (notification + full-screen stay) so answer/decline still work. Co-Authored-By: Claude Opus 4.8 --- .../nativecall/IncomingCallActivity.java | 15 ++++++ .../bizgaze/connect/nativecall/Ringer.java | 51 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/mobile/plugins/native-call/android/src/main/java/com/bizgaze/connect/nativecall/IncomingCallActivity.java b/mobile/plugins/native-call/android/src/main/java/com/bizgaze/connect/nativecall/IncomingCallActivity.java index 633cdcb..265bc31 100644 --- a/mobile/plugins/native-call/android/src/main/java/com/bizgaze/connect/nativecall/IncomingCallActivity.java +++ b/mobile/plugins/native-call/android/src/main/java/com/bizgaze/connect/nativecall/IncomingCallActivity.java @@ -9,6 +9,7 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Gravity; +import android.view.KeyEvent; import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; @@ -214,6 +215,20 @@ public class IncomingCallActivity extends Activity { finish(); } + // Volume keys silence the ring (like the system phone app) while the full-screen screen is in front. We + // consume the event so the stream volume / volume UI doesn't also react — the press only hushes the ring; the + // call keeps ringing (notification + screen stay) so the user can still answer/decline. The headless case + // (unlocked heads-up, no activity) is handled by Ringer's MediaSession VolumeProvider. + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + int k = event.getKeyCode(); + if (k == KeyEvent.KEYCODE_VOLUME_UP || k == KeyEvent.KEYCODE_VOLUME_DOWN || k == KeyEvent.KEYCODE_VOLUME_MUTE) { + if (event.getAction() == KeyEvent.ACTION_DOWN) Ringer.silence(); + return true; // consume down + up so the volume stream doesn't change + } + return super.dispatchKeyEvent(event); + } + @Override protected void onDestroy() { // Don't stop the ring here — it's call-scoped (Ringer), not screen-scoped. If the user just backgrounds the diff --git a/mobile/plugins/native-call/android/src/main/java/com/bizgaze/connect/nativecall/Ringer.java b/mobile/plugins/native-call/android/src/main/java/com/bizgaze/connect/nativecall/Ringer.java index 118f4fe..3d1527c 100644 --- a/mobile/plugins/native-call/android/src/main/java/com/bizgaze/connect/nativecall/Ringer.java +++ b/mobile/plugins/native-call/android/src/main/java/com/bizgaze/connect/nativecall/Ringer.java @@ -4,6 +4,9 @@ import android.content.Context; import android.media.AudioAttributes; import android.media.Ringtone; import android.media.RingtoneManager; +import android.media.VolumeProvider; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; import android.net.Uri; import android.os.Build; import android.os.Vibrator; @@ -18,6 +21,10 @@ import android.util.Log; class Ringer { private static Ringtone ringtone; private static Vibrator vibrator; + // A dummy MediaSession whose only job is to intercept the hardware volume keys so the FIRST press silences the + // ring (see startVolumeKeyCapture). We aren't a telecom ConnectionService yet (Increment 2), so the OS won't + // route volume/power to us on its own. + private static MediaSession mediaSession; static synchronized void start(Context ctx) { if (ringtone != null) return; // already ringing @@ -51,6 +58,44 @@ class Ringer { vibrator = v; } } catch (Exception ignored) {} + startVolumeKeyCapture(app); + } + + // Make the hardware volume keys silence the ring, exactly like the system phone app. Because we are not a + // telecom ConnectionService (that's Increment 2), the OS won't deliver volume/power presses to us while + // ringing. A MediaSession flagged as actively "playing" with a *remote* VolumeProvider makes the framework + // route volume-key presses to our VolumeProvider instead of changing a stream — even when no activity of ours + // is in the foreground (the unlocked heads-up case). The foreground/locked full-screen activity also forwards + // its own key events here via dispatchKeyEvent, so every state is covered. + private static void startVolumeKeyCapture(Context app) { + try { + MediaSession s = new MediaSession(app, "BzCallRinger"); + s.setPlaybackState(new PlaybackState.Builder() + .setState(PlaybackState.STATE_PLAYING, 0, 1f) + .setActions(0) + .build()); + VolumeProvider vp = new VolumeProvider(VolumeProvider.VOLUME_CONTROL_RELATIVE, 100, 100) { + @Override public void onAdjustVolume(int direction) { silence(); } + @Override public void onSetVolumeTo(int volume) { silence(); } + }; + s.setPlaybackToRemote(vp); + s.setActive(true); + mediaSession = s; + Log.i("bzcall", "volume-key capture armed"); + } catch (Exception e) { Log.w("bzcall", "volume-key capture failed: " + e.getMessage()); } + } + + // Stop the NOISE (audio + vibration) but leave the call ringing visually — the notification and the + // full-screen screen stay up so the user can still answer/decline. This is what a volume/power press does on a + // real phone: it hushes the ringtone, it does not reject the call. Idempotent. + static synchronized void silence() { + boolean wasRinging = ringtone != null || vibrator != null || mediaSession != null; + try { if (ringtone != null) ringtone.stop(); } catch (Exception ignored) {} + ringtone = null; + try { if (vibrator != null) vibrator.cancel(); } catch (Exception ignored) {} + vibrator = null; + releaseSession(); + if (wasRinging) Log.i("bzcall", "ringer silenced (volume/power)"); } static synchronized void stop() { @@ -58,5 +103,11 @@ class Ringer { ringtone = null; try { if (vibrator != null) vibrator.cancel(); } catch (Exception ignored) {} vibrator = null; + releaseSession(); + } + + private static void releaseSession() { + try { if (mediaSession != null) { mediaSession.setActive(false); mediaSession.release(); } } catch (Exception ignored) {} + mediaSession = null; } }