Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ android {
applicationId "com.winnative.cmod"
minSdk 26
targetSdk 28
versionCode 1
versionCode 2
versionName appVersionName

externalNativeBuild {
Expand Down Expand Up @@ -183,15 +183,10 @@ android {
excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
}
jniLibs {
// PulseAudio runs libpulseaudio.so directly from nativeLibraryDir and loads
// libpulse*/libltdl/libsndfile as its dependencies, so these must be packaged
// and extracted to disk (useLegacyPackaging) rather than excluded.
useLegacyPackaging = true
excludes += [
"lib/arm64-v8a/libltdl.so",
"lib/arm64-v8a/libpulseaudio.so",
"lib/arm64-v8a/libpulse.so",
"lib/arm64-v8a/libpulsecommon-13.0.so",
"lib/arm64-v8a/libpulsecore-13.0.so",
"lib/arm64-v8a/libsndfile.so"
]
}
}

Expand Down
Binary file removed app/src/main/assets/pulseaudio-bin/libltdl.so
Binary file not shown.
Binary file removed app/src/main/assets/pulseaudio-bin/libpulse.so
Binary file not shown.
Binary file removed app/src/main/assets/pulseaudio-bin/libpulseaudio.so
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed app/src/main/assets/pulseaudio-bin/libsndfile.so
Binary file not shown.
Binary file modified app/src/main/assets/pulseaudio.tzst
Binary file not shown.
Binary file modified app/src/main/jniLibs/arm64-v8a/libltdl.so
Binary file not shown.
Binary file modified app/src/main/jniLibs/arm64-v8a/libpulse.so
Binary file not shown.
Binary file modified app/src/main/jniLibs/arm64-v8a/libpulseaudio.so
Binary file not shown.
Binary file modified app/src/main/jniLibs/arm64-v8a/libpulsecommon-13.0.so
Binary file not shown.
Binary file modified app/src/main/jniLibs/arm64-v8a/libpulsecore-13.0.so
Binary file not shown.
Binary file modified app/src/main/jniLibs/arm64-v8a/libsndfile.so
Binary file not shown.
16 changes: 16 additions & 0 deletions app/src/main/runtime/display/XServerDisplayActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ public class XServerDisplayActivity extends FixedFontScaleAppCompatActivity {
private boolean controllerAutoHidden = false;
private boolean userOverrodeAutoHide = false;
private XEnvironment environment;
private com.winlator.cmod.runtime.display.environment.AudioFocusHandler audioFocusHandler;
private ComposeView displayHostComposeView;
private FrameLayout xServerDisplayFrame;
private ContainerManager containerManager;
Expand Down Expand Up @@ -2341,6 +2342,15 @@ private int[] getCapturedPointerDelta(MotionEvent event) {
};
}

private void ensureAudioFocusHandler() {
if (audioFocusHandler != null) return;
audioFocusHandler =
new com.winlator.cmod.runtime.display.environment.AudioFocusHandler(
this,
() -> { if (environment != null) environment.onPause(); },
() -> { if (environment != null) environment.onResume(); });
}

@Override
public void onResume() {
super.onResume();
Expand All @@ -2353,6 +2363,8 @@ public void onResume() {
if (!cleaningUp && environment != null) {
xServerView.onResume();
environment.onResume();
ensureAudioFocusHandler();
if (audioFocusHandler != null) audioFocusHandler.request();
}

if (inputControlsView != null && touchpadView != null) {
Expand Down Expand Up @@ -3748,6 +3760,10 @@ protected void onDestroy() {
externalDisplayController.release();
externalDisplayController = null;
}
if (audioFocusHandler != null) {
audioFocusHandler.release();
audioFocusHandler = null;
}
if (isDependencyInstall) {
com.winlator.cmod.runtime.content.component.DependencyInstallBridge.complete(dependencyExitStatus);
}
Expand Down
107 changes: 107 additions & 0 deletions app/src/main/runtime/display/environment/AudioFocusHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.winlator.cmod.runtime.display.environment;

import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.util.Log;

/**
* Complements the activity lifecycle for suspending/resuming guest audio around interruptions that
* do not cleanly pause the activity — most importantly incoming phone calls and VoIP calls.
*
* <p>The activity's onPause/onResume already suspend/resume audio, but a phone call arrives as a
* transient audio-focus loss and, when it ends, we get a precise focus-gain callback which is a more
* reliable "the call is over, restore audio now" signal than waiting for the activity to be brought
* back to the foreground. The suspend/resume callbacks here are idempotent with the lifecycle ones,
* so firing from both paths is safe.
*
* <p>Focus callbacks are delivered on the main thread; the suspend/resume work touches unix sockets,
* so it is dispatched to a background thread to keep the UI responsive.
*/
public class AudioFocusHandler {
private static final String TAG = "AudioFocusHandler";

private final AudioManager audioManager;
private final Runnable onSuspend;
private final Runnable onResume;
private final AudioFocusRequest focusRequest;

private boolean requested = false;
private boolean suspendedByFocusLoss = false;

public AudioFocusHandler(Context context, Runnable onSuspend, Runnable onResume) {
this.audioManager = (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
this.onSuspend = onSuspend;
this.onResume = onResume;

AudioAttributes attributes =
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
this.focusRequest =
new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(attributes)
.setWillPauseWhenDucked(false)
.setOnAudioFocusChangeListener(this::onFocusChange)
.build();
}

/** Requests audio focus so we start receiving focus-change callbacks. Safe to call repeatedly. */
public synchronized void request() {
if (audioManager == null || requested) return;
try {
int result = audioManager.requestAudioFocus(focusRequest);
requested = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
} catch (Exception e) {
Log.w(TAG, "requestAudioFocus failed: " + e.getMessage());
}
}

/** Abandons audio focus and stops callbacks. Call on teardown. */
public synchronized void release() {
if (audioManager == null || !requested) return;
try {
audioManager.abandonAudioFocusRequest(focusRequest);
} catch (Exception e) {
Log.w(TAG, "abandonAudioFocus failed: " + e.getMessage());
}
requested = false;
suspendedByFocusLoss = false;
}

private void onFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// Phone call / another app took over playback. Release the AAudio device.
synchronized (this) {
suspendedByFocusLoss = true;
}
dispatch(onSuspend);
break;
case AudioManager.AUDIOFOCUS_GAIN:
// Interruption ended; restore output. Only if we were the ones who suspended, so we
// don't fight a resume the activity lifecycle is already driving.
boolean shouldResume;
synchronized (this) {
shouldResume = suspendedByFocusLoss;
suspendedByFocusLoss = false;
}
if (shouldResume) dispatch(onResume);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
default:
// Ducking / unknown: leave playback alone.
break;
}
}

private static void dispatch(Runnable task) {
if (task == null) return;
Thread thread = new Thread(task, "AudioFocusDispatch");
thread.setDaemon(true);
thread.start();
}
}
2 changes: 1 addition & 1 deletion app/src/main/runtime/display/environment/XEnvironment.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ public void onPause() {
}

public void onResume() {
ALSAClient.setOutputSuspended(false);
PulseAudioComponent pulseAudio = getComponent(PulseAudioComponent.class);
if (pulseAudio != null) pulseAudio.resume();
ALSAClient.setOutputSuspended(false);
}
}
Loading