mirror of
https://github.com/Benjamin-Wiegand/Flywheel.git
synced 2026-08-07 14:22:23 -07:00
feat: pause media audio while playing notification tts
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+22
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user