diff --git a/app/src/main/java/com/winlator/audio/AudioMonitor.java b/app/src/main/java/com/winlator/audio/AudioMonitor.java new file mode 100644 index 000000000..ced544816 --- /dev/null +++ b/app/src/main/java/com/winlator/audio/AudioMonitor.java @@ -0,0 +1,109 @@ +package com.winlator.audio; + +import static android.media.AudioManager.GET_DEVICES_OUTPUTS; + +import android.content.Context; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import com.winlator.xenvironment.XEnvironment; +import com.winlator.xenvironment.components.ALSAServerComponent; +import com.winlator.xenvironment.components.PulseAudioComponent; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +import timber.log.Timber; + +public class AudioMonitor { + + private final XEnvironment xEnvironment; + private final AudioManager audioManager; + private boolean audioCallbackRegistered = false; + private final Handler restartHandler = new Handler(Looper.getMainLooper()); + private final Runnable restartRunnable = this::restartAudioComponent; + private final int restartDelay = 500; + + public AudioMonitor(XEnvironment xEnvironment, Context context) { + this.xEnvironment = xEnvironment; + this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + this.audioCallbackRegistered = true; + this.audioManager.registerAudioDeviceCallback(audioDeviceCallback, null); + } + + private final AudioDeviceCallback audioDeviceCallback = new AudioDeviceCallback() { + @Override + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + // Handle newly added audio devices (e.g., headphones connected) + try { + restartHandler.removeCallbacks(restartRunnable); + } catch (Exception ignored) { + } + + Timber.d("onAudioDevicesAdded: %s", Arrays.toString(Arrays.stream(addedDevices) + .map(device -> device.getProductName() + "-" + device.getType()).toArray())); + + // Check if any new device is sink and all new devices are not in skippedDeviceTypes + if (Arrays.stream(addedDevices).anyMatch(AudioDeviceInfo::isSink)) { + restartHandler.postDelayed(restartRunnable, restartDelay); + } + } + + @Override + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + // Handle removed audio devices (e.g., headphones disconnected) + try { + restartHandler.removeCallbacks(restartRunnable); + } catch (Exception ignored) { + } + + Timber.d("onAudioDevicesRemoved: %s", Arrays.toString(Arrays.stream(removedDevices) + .map(device -> device.getProductName() + "-" + device.getType()).toArray())); + + // Check if any removed device is sink and all removed devices are not in skippedDeviceTypes + if (Arrays.stream(removedDevices).anyMatch(AudioDeviceInfo::isSink)) { + restartHandler.postDelayed(restartRunnable, restartDelay); + } + } + }; + + private void restartAudioComponent() { + // if it is android 15 or above skip restart + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + return; + } + + final ALSAServerComponent alsaServerComponent = xEnvironment.getComponent(ALSAServerComponent.class); + if (alsaServerComponent != null) { + alsaServerComponent.stop(); + alsaServerComponent.start(); + } + + final PulseAudioComponent pulseAudioComponent = xEnvironment.getComponent(PulseAudioComponent.class); + if (pulseAudioComponent != null) { + //pulseAudioComponent.stop(); stop is already called inside start function + pulseAudioComponent.start(); + } + } + + public void onPause() { + if (audioCallbackRegistered) { + audioManager.unregisterAudioDeviceCallback(audioDeviceCallback); + audioCallbackRegistered = false; + } + } + + public void onResume() { + if (!audioCallbackRegistered) { + audioManager.registerAudioDeviceCallback(audioDeviceCallback, null); + audioCallbackRegistered = true; + } + } +} diff --git a/app/src/main/java/com/winlator/xenvironment/XEnvironment.java b/app/src/main/java/com/winlator/xenvironment/XEnvironment.java index a827a3a9c..35e2424b2 100644 --- a/app/src/main/java/com/winlator/xenvironment/XEnvironment.java +++ b/app/src/main/java/com/winlator/xenvironment/XEnvironment.java @@ -1,19 +1,12 @@ package com.winlator.xenvironment; import android.content.Context; -import android.media.AudioDeviceCallback; -import android.media.AudioDeviceInfo; -import android.media.AudioManager; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; +import com.winlator.audio.AudioMonitor; import com.winlator.core.FileUtils; -import com.winlator.xenvironment.components.ALSAServerComponent; import com.winlator.xenvironment.components.BionicProgramLauncherComponent; import com.winlator.xenvironment.components.GlibcProgramLauncherComponent; import com.winlator.xenvironment.components.GuestProgramLauncherComponent; -import com.winlator.xenvironment.components.PulseAudioComponent; import java.io.File; import java.util.ArrayList; @@ -26,29 +19,7 @@ public class XEnvironment implements Iterable { private boolean winetricksRunning = false; - private final AudioManager audioManager; - private boolean audioCallbackRegistered = false; - private final AudioDeviceCallback audioDeviceCallback = new AudioDeviceCallback() { - @Override - public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { - // Handle newly added audio devices (e.g., headphones connected) - for (AudioDeviceInfo device : addedDevices) { - if (device.isSink()) { - restartAudioComponent(); - } - } - } - - @Override - public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { - // Handle removed audio devices (e.g., headphones disconnected) - for (AudioDeviceInfo device : removedDevices) { - if (device.isSink()) { - restartAudioComponent(); - } - } - } - }; + private final AudioMonitor audioMonitor; public synchronized boolean isWinetricksRunning() { return winetricksRunning; @@ -61,9 +32,7 @@ public synchronized void setWinetricksRunning(boolean running) { public XEnvironment(Context context, ImageFs imageFs) { this.context = context; this.imageFs = imageFs; - this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - this.audioManager.registerAudioDeviceCallback(audioDeviceCallback, null); - this.audioCallbackRegistered = true; + this.audioMonitor = new AudioMonitor(this, context); } public Context getContext() { @@ -110,6 +79,8 @@ public void stopEnvironmentComponents() { } public void onPause() { + audioMonitor.onPause(); + GuestProgramLauncherComponent guestProgramLauncherComponent = getComponent(GuestProgramLauncherComponent.class); if (guestProgramLauncherComponent != null) guestProgramLauncherComponent.suspendProcess(); GlibcProgramLauncherComponent glibcProgramLauncherComponent = getComponent(GlibcProgramLauncherComponent.class); @@ -119,6 +90,8 @@ public void onPause() { } public void onResume() { + audioMonitor.onResume(); + GuestProgramLauncherComponent guestProgramLauncherComponent = getComponent(GuestProgramLauncherComponent.class); if (guestProgramLauncherComponent != null) guestProgramLauncherComponent.resumeProcess(); GlibcProgramLauncherComponent glibcProgramLauncherComponent = getComponent(GlibcProgramLauncherComponent.class); @@ -126,18 +99,4 @@ public void onResume() { BionicProgramLauncherComponent bionicProgramLauncherComponent = getComponent(BionicProgramLauncherComponent.class); if (bionicProgramLauncherComponent != null) bionicProgramLauncherComponent.resumeProcess(); } - - private void restartAudioComponent() { - final ALSAServerComponent alsaServerComponent = getComponent(ALSAServerComponent.class); - if (alsaServerComponent != null) { - alsaServerComponent.stop(); - alsaServerComponent.start(); - } - - final PulseAudioComponent pulseAudioComponent = getComponent(PulseAudioComponent.class); - if (pulseAudioComponent != null) { - //pulseAudioComponent.stop(); stop is already called inside start function - pulseAudioComponent.start(); - } - } }