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 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ec6c3f227
commit
aa794102c3
2 files changed
+66
No files matched your search
+15
@@ -9,6 +9,7 @@ import android.os.Build;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
@@ -214,6 +215,20 @@ public class IncomingCallActivity extends Activity {
|
|||||||
finish();
|
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
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
// Don't stop the ring here — it's call-scoped (Ringer), not screen-scoped. If the user just backgrounds the
|
// Don't stop the ring here — it's call-scoped (Ringer), not screen-scoped. If the user just backgrounds the
|
||||||
|
|||||||
+51
@@ -4,6 +4,9 @@ import android.content.Context;
|
|||||||
import android.media.AudioAttributes;
|
import android.media.AudioAttributes;
|
||||||
import android.media.Ringtone;
|
import android.media.Ringtone;
|
||||||
import android.media.RingtoneManager;
|
import android.media.RingtoneManager;
|
||||||
|
import android.media.VolumeProvider;
|
||||||
|
import android.media.session.MediaSession;
|
||||||
|
import android.media.session.PlaybackState;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Vibrator;
|
import android.os.Vibrator;
|
||||||
@@ -18,6 +21,10 @@ import android.util.Log;
|
|||||||
class Ringer {
|
class Ringer {
|
||||||
private static Ringtone ringtone;
|
private static Ringtone ringtone;
|
||||||
private static Vibrator vibrator;
|
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) {
|
static synchronized void start(Context ctx) {
|
||||||
if (ringtone != null) return; // already ringing
|
if (ringtone != null) return; // already ringing
|
||||||
@@ -51,6 +58,44 @@ class Ringer {
|
|||||||
vibrator = v;
|
vibrator = v;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} 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() {
|
static synchronized void stop() {
|
||||||
@@ -58,5 +103,11 @@ class Ringer {
|
|||||||
ringtone = null;
|
ringtone = null;
|
||||||
try { if (vibrator != null) vibrator.cancel(); } catch (Exception ignored) {}
|
try { if (vibrator != null) vibrator.cancel(); } catch (Exception ignored) {}
|
||||||
vibrator = null;
|
vibrator = null;
|
||||||
|
releaseSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void releaseSession() {
|
||||||
|
try { if (mediaSession != null) { mediaSession.setActive(false); mediaSession.release(); } } catch (Exception ignored) {}
|
||||||
|
mediaSession = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user