feat: pause media audio while playing notification tts

This commit is contained in:
Benjamin Wiegand
2026-06-26 12:31:24 -07:00
parent c18ee8bcce
commit 3a4cba374a
2 changed files with 130 additions and 0 deletions
@@ -2,14 +2,23 @@ package io.benwiegand.projection.geargrinder;
import android.content.ComponentName;
import android.content.Intent;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.function.Consumer;
import io.benwiegand.projection.geargrinder.callback.MakeshiftBindCallback;
@@ -18,11 +27,19 @@ import io.benwiegand.projection.geargrinder.makeshiftbind.MakeshiftBind;
public class NotificationService extends NotificationListenerService implements MakeshiftBindCallback {
private static final String TAG = NotificationService.class.getSimpleName();
private static final long MEDIA_INTERRUPTION_END_DELAY = 500;
private final Handler handler = new Handler(Looper.getMainLooper());
private final ServiceBinder binder = new ServiceBinder();
private MakeshiftBind makeshiftBind = null;
private final Queue<NotificationListener> listeners = new LinkedList<>();
private final Set<String> activeMediaInterruptions = new HashSet<>();
private MediaController interruptedMediaSessionController = null;
private Object interruptionToken = new Object();
public interface NotificationListener {
void onNotificationPosted(StatusBarNotification sbn);
}
@@ -75,6 +92,89 @@ public class NotificationService extends NotificationListenerService implements
callListeners(l -> l.onNotificationPosted(sbn));
}
private MediaController findActiveMediaSessionController() {
MediaSessionManager mediaSessionManager = getSystemService(MediaSessionManager.class);
List<MediaController> sessions = mediaSessionManager.getActiveSessions(new ComponentName(this, NotificationService.class));
for (MediaController controller : sessions) {
PlaybackState playbackState = controller.getPlaybackState();
if (playbackState == null) continue;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!playbackState.isActive()) continue;
} else if (playbackState.getState() == PlaybackState.STATE_PLAYING) {
continue;
}
return controller;
}
return null;
}
private void beginMediaInterruption(String interruptionId) {
synchronized (activeMediaInterruptions) {
Log.d(TAG, "adding interruption: " + interruptionId);
boolean interruptionStart = activeMediaInterruptions.isEmpty();
activeMediaInterruptions.add(interruptionId);
if (!interruptionStart) return;
Log.i(TAG, "starting media interruption");
interruptionToken = new Object();
if (interruptedMediaSessionController != null) {
Log.d(TAG, "continuing previous interruption");
return;
}
interruptedMediaSessionController = findActiveMediaSessionController();
if (interruptedMediaSessionController == null) {
Log.d(TAG, "no active media session");
return;
}
try {
interruptedMediaSessionController.getTransportControls().pause();
} catch (Throwable t) {
Log.e(TAG, "failed to pause media", t);
}
}
}
private void endMediaInterruption(String interruptionId) {
synchronized (activeMediaInterruptions) {
Log.d(TAG, "removing interruption: " + interruptionId);
if (!activeMediaInterruptions.remove(interruptionId)) {
Log.e(TAG, "no such interruption: " + interruptionId);
return;
}
if (!activeMediaInterruptions.isEmpty()) return;
Log.i(TAG, "scheduling end of media interruption");
Object token = interruptionToken;
handler.postDelayed(() -> {
synchronized (activeMediaInterruptions) {
if (!activeMediaInterruptions.isEmpty()) return;
if (token != interruptionToken) return;
Log.i(TAG, "stopping media interruption");
if (interruptedMediaSessionController == null) {
Log.d(TAG, "nothing to resume");
return;
}
try {
interruptedMediaSessionController.getTransportControls().play();
} catch (Throwable t) {
Log.e(TAG, "failed to resume media", t);
}
interruptedMediaSessionController = null;
}
}, MEDIA_INTERRUPTION_END_DELAY);
}
}
public class ServiceBinder extends Binder {
@@ -86,6 +186,14 @@ public class NotificationService extends NotificationListenerService implements
listeners.remove(listener);
}
public void beginMediaInterruption(String interruptionId) {
NotificationService.this.beginMediaInterruption(interruptionId);
}
public void endMediaInterruption(String interruptionId) {
NotificationService.this.endMediaInterruption(interruptionId);
}
}
@@ -9,6 +9,7 @@ import android.os.Handler;
import android.os.Looper;
import android.service.notification.StatusBarNotification;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import android.util.StateSet;
import android.view.LayoutInflater;
@@ -34,6 +35,8 @@ import io.benwiegand.projection.geargrinder.pm.AppRecord;
public class NotificationDisplay implements NotificationService.NotificationListener {
private static final String TAG = NotificationDisplay.class.getSimpleName();
private static final String TTS_INTERRUPTION_ID = TAG;
private static final long TTS_ANNOUNCEMENT_PAUSE = 1500; // milliseconds to pause between queued TTS messages
private static final long POPUP_NOTIFICATION_ANIMATION_DURATION = 200;
private static final long POPUP_NOTIFICATION_SHOW_DURATION = 10000;
@@ -66,6 +69,25 @@ public class NotificationDisplay implements NotificationService.NotificationList
popupNotificationOverlay.setOnClickListener(v -> dismissTopNotification());
tts = new TextToSpeech(context, this::onTTSInit);
tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onDone(String utteranceId) {
Log.d(TAG, "tts finished for utterance: " + utteranceId);
getNotificationServiceBinder().ifPresent(ns -> ns.endMediaInterruption(TTS_INTERRUPTION_ID));
}
@Override
public void onError(String utteranceId) {
Log.e(TAG, "tts error for utterance: " + utteranceId);
getNotificationServiceBinder().ifPresent(ns -> ns.endMediaInterruption(TTS_INTERRUPTION_ID));
}
@Override
public void onStart(String utteranceId) {
Log.d(TAG, "tts started for utterance: " + utteranceId);
getNotificationServiceBinder().ifPresent(ns -> ns.beginMediaInterruption(TTS_INTERRUPTION_ID));
}
});
}
public void destroy() {