diff --git a/app/build.gradle b/app/build.gradle index 12c190c14..9a83a7dea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,7 @@ android { applicationId "com.winnative.cmod" minSdk 26 targetSdk 28 - versionCode 1 + versionCode 2 versionName appVersionName externalNativeBuild { @@ -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" - ] } } diff --git a/app/src/main/assets/pulseaudio-bin/libltdl.so b/app/src/main/assets/pulseaudio-bin/libltdl.so deleted file mode 100644 index 48b910a2a..000000000 Binary files a/app/src/main/assets/pulseaudio-bin/libltdl.so and /dev/null differ diff --git a/app/src/main/assets/pulseaudio-bin/libpulse.so b/app/src/main/assets/pulseaudio-bin/libpulse.so deleted file mode 100644 index bc8b723e0..000000000 Binary files a/app/src/main/assets/pulseaudio-bin/libpulse.so and /dev/null differ diff --git a/app/src/main/assets/pulseaudio-bin/libpulseaudio.so b/app/src/main/assets/pulseaudio-bin/libpulseaudio.so deleted file mode 100644 index 59a044255..000000000 Binary files a/app/src/main/assets/pulseaudio-bin/libpulseaudio.so and /dev/null differ diff --git a/app/src/main/assets/pulseaudio-bin/libpulsecommon-13.0.so b/app/src/main/assets/pulseaudio-bin/libpulsecommon-13.0.so deleted file mode 100644 index ce2e949f5..000000000 Binary files a/app/src/main/assets/pulseaudio-bin/libpulsecommon-13.0.so and /dev/null differ diff --git a/app/src/main/assets/pulseaudio-bin/libpulsecore-13.0.so b/app/src/main/assets/pulseaudio-bin/libpulsecore-13.0.so deleted file mode 100644 index 031ff579f..000000000 Binary files a/app/src/main/assets/pulseaudio-bin/libpulsecore-13.0.so and /dev/null differ diff --git a/app/src/main/assets/pulseaudio-bin/libsndfile.so b/app/src/main/assets/pulseaudio-bin/libsndfile.so deleted file mode 100644 index dcf49b7fa..000000000 Binary files a/app/src/main/assets/pulseaudio-bin/libsndfile.so and /dev/null differ diff --git a/app/src/main/assets/pulseaudio.tzst b/app/src/main/assets/pulseaudio.tzst index 076a47a3e..7cdea4405 100644 Binary files a/app/src/main/assets/pulseaudio.tzst and b/app/src/main/assets/pulseaudio.tzst differ diff --git a/app/src/main/jniLibs/arm64-v8a/libltdl.so b/app/src/main/jniLibs/arm64-v8a/libltdl.so index 48b910a2a..5cd08c89d 100644 Binary files a/app/src/main/jniLibs/arm64-v8a/libltdl.so and b/app/src/main/jniLibs/arm64-v8a/libltdl.so differ diff --git a/app/src/main/jniLibs/arm64-v8a/libpulse.so b/app/src/main/jniLibs/arm64-v8a/libpulse.so index bc8b723e0..3443f4d4c 100644 Binary files a/app/src/main/jniLibs/arm64-v8a/libpulse.so and b/app/src/main/jniLibs/arm64-v8a/libpulse.so differ diff --git a/app/src/main/jniLibs/arm64-v8a/libpulseaudio.so b/app/src/main/jniLibs/arm64-v8a/libpulseaudio.so index 59a044255..bccfd71c8 100644 Binary files a/app/src/main/jniLibs/arm64-v8a/libpulseaudio.so and b/app/src/main/jniLibs/arm64-v8a/libpulseaudio.so differ diff --git a/app/src/main/jniLibs/arm64-v8a/libpulsecommon-13.0.so b/app/src/main/jniLibs/arm64-v8a/libpulsecommon-13.0.so index ce2e949f5..f09b73d0f 100644 Binary files a/app/src/main/jniLibs/arm64-v8a/libpulsecommon-13.0.so and b/app/src/main/jniLibs/arm64-v8a/libpulsecommon-13.0.so differ diff --git a/app/src/main/jniLibs/arm64-v8a/libpulsecore-13.0.so b/app/src/main/jniLibs/arm64-v8a/libpulsecore-13.0.so index 031ff579f..9f932277b 100644 Binary files a/app/src/main/jniLibs/arm64-v8a/libpulsecore-13.0.so and b/app/src/main/jniLibs/arm64-v8a/libpulsecore-13.0.so differ diff --git a/app/src/main/jniLibs/arm64-v8a/libsndfile.so b/app/src/main/jniLibs/arm64-v8a/libsndfile.so index dcf49b7fa..f7ab9d12b 100644 Binary files a/app/src/main/jniLibs/arm64-v8a/libsndfile.so and b/app/src/main/jniLibs/arm64-v8a/libsndfile.so differ diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index fce78b5d0..cf4a7d7f4 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -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; @@ -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(); @@ -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) { @@ -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); } diff --git a/app/src/main/runtime/display/environment/AudioFocusHandler.java b/app/src/main/runtime/display/environment/AudioFocusHandler.java new file mode 100644 index 000000000..d9bb1afd1 --- /dev/null +++ b/app/src/main/runtime/display/environment/AudioFocusHandler.java @@ -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. + * + *

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. + * + *

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(); + } +} diff --git a/app/src/main/runtime/display/environment/XEnvironment.java b/app/src/main/runtime/display/environment/XEnvironment.java index f95974f9a..ea34ccd1c 100644 --- a/app/src/main/runtime/display/environment/XEnvironment.java +++ b/app/src/main/runtime/display/environment/XEnvironment.java @@ -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); } } diff --git a/app/src/main/runtime/display/environment/components/PulseAudioComponent.java b/app/src/main/runtime/display/environment/components/PulseAudioComponent.java index fbbf06b97..e30cd7b47 100644 --- a/app/src/main/runtime/display/environment/components/PulseAudioComponent.java +++ b/app/src/main/runtime/display/environment/components/PulseAudioComponent.java @@ -1,28 +1,48 @@ package com.winlator.cmod.runtime.display.environment.components; import android.content.Context; -import android.media.AudioManager; -import android.os.Process; +import android.util.Log; import com.winlator.cmod.runtime.display.connector.UnixSocketConfig; import com.winlator.cmod.runtime.display.environment.EnvironmentComponent; import com.winlator.cmod.runtime.system.ProcessHelper; import com.winlator.cmod.runtime.wine.EnvVars; -import com.winlator.cmod.shared.android.AppUtils; import com.winlator.cmod.shared.io.FileUtils; +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +/** + * PulseAudio server management. + * + *

This mirrors the GameNative PulseAudio stack, which is the coherent, maintained build that + * reliably resumes audio after interruptions such as phone calls. The pieces that make resume work + * live in the native binaries, not here: + * + *

+ * + *

Pause/resume simply suspend and un-suspend the sink via {@code pactl suspend-sink}; the module + * closes the AAudio device on suspend and reopens it on resume. + */ public class PulseAudioComponent extends EnvironmentComponent { + private static final String TAG = "PulseAudioComponent"; + private static final String SINK_NAME = "AAudioSink"; + private final UnixSocketConfig socketConfig; private final Options options; - private static int pid = -1; private static final Object lock = new Object(); + private boolean isPaused = false; public PulseAudioComponent(UnixSocketConfig socketConfig) { this(socketConfig, new Options()); @@ -34,7 +54,7 @@ public PulseAudioComponent(UnixSocketConfig socketConfig, Options options) { } public static class Options { - public static final int DEFAULT_LATENCY_MILLIS = 144; + public static final int DEFAULT_LATENCY_MILLIS = 40; public static final int DEFAULT_FRAGMENT_MILLIS = 10; public static final int DEFAULT_SAMPLE_RATE = 48000; public static final int DEFAULT_ALTERNATE_SAMPLE_RATE = 44100; @@ -156,221 +176,176 @@ private static float parseFloat(String value, float fallback) { @Override public void start() { synchronized (lock) { - stop(); - pid = execPulseAudio(); + if (!isServerRunning()) { + killAllPulseAudioProcesses(); + startPulseAudio(); + isPaused = false; + } } } @Override public void stop() { synchronized (lock) { - if (pid != -1) { - Process.killProcess(pid); - pid = -1; - } + updateSink(true); + isPaused = false; + killAllPulseAudioProcesses(); } } public void suspend() { synchronized (lock) { - if (pid != -1) ProcessHelper.suspendProcess(pid); + if (!isPaused && isServerRunning()) { + isPaused = true; + updateSink(true); + } } } public void resume() { synchronized (lock) { - if (pid != -1) ProcessHelper.resumeProcess(pid); + if (isPaused) { + if (isServerRunning()) { + isPaused = false; + updateSink(false); + } else { + // Daemon died while backgrounded; relaunch it. default.pa re-creates the sink. + start(); + } + } } } - private void copyFromLibraryDir(File dst) { - String[] libs = - new String[] { - "libltdl.so", - "libpulseaudio.so", - "libpulse.so", - "libpulsecommon-13.0.so", - "libpulsecore-13.0.so", - "libsndfile.so" - }; - for (int i = 0; i < libs.length; i++) { - Path dstDir = Paths.get(dst.getAbsolutePath() + "/" + libs[i]); - try (InputStream is = - environment.getContext().getAssets().open("pulseaudio-bin/" + libs[i])) { - if (is != null) { - Files.copy(is, dstDir, StandardCopyOption.REPLACE_EXISTING); - FileUtils.chmod(dstDir.toFile(), 0771); + public boolean isServerRunning() { + String info = execPactlCommand("info").toLowerCase(java.util.Locale.ROOT); + return info.contains("server name:") && !info.contains("connection failure"); + } + + private void updateSink(boolean suspend) { + execPactlCommand("suspend-sink " + SINK_NAME + " " + (suspend ? "true" : "false")); + } + + private void killAllPulseAudioProcesses() { + File proc = new File("/proc"); + String[] allPids = + proc.list((dir, name) -> new File(dir, name).isDirectory() && name.matches("[0-9]+")); + if (allPids == null) return; + boolean killed = false; + for (String pidStr : allPids) { + String cmdline = readProcCmdline(pidStr); + if (cmdline.contains("libpulseaudio.so")) { + try { + ProcessHelper.killProcess(Integer.parseInt(pidStr)); + killed = true; + } catch (NumberFormatException ignored) { } - } catch (IOException e) { - throw new RuntimeException(e); + } + } + if (killed) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } } - private int execPulseAudio() { + private static String readProcCmdline(String pid) { + try (FileInputStream fr = new FileInputStream("/proc/" + pid + "/cmdline")) { + byte[] bytes = fr.readAllBytes(); + return new String(bytes, StandardCharsets.UTF_8).replace('\0', ' '); + } catch (IOException e) { + return ""; + } + } + + private void startPulseAudio() { Context context = environment.getContext(); + String nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir; + File workingDir = new File(context.getFilesDir(), "/pulseaudio"); if (!workingDir.isDirectory()) { workingDir.mkdirs(); FileUtils.chmod(workingDir, 0771); } - File configDir = new File(workingDir, ".config/pulse"); - if (!configDir.isDirectory()) configDir.mkdirs(); - File runtimeDir = new File(workingDir, "run"); - if (!runtimeDir.isDirectory()) runtimeDir.mkdirs(); + // Clear any stale runtime state (e.g. cookie) from a previous run. + File configDir = new File(workingDir, ".config"); + if (configDir.exists()) FileUtils.delete(configDir); - int sampleRate = - options.sampleRateOverridden ? options.sampleRate : getNativeOutputSampleRate(context); - int alternateSampleRate = - options.alternateSampleRateOverridden - ? options.alternateSampleRate - : getAlternateSampleRate(sampleRate); - String channelMap = getChannelMap(options.channels); - - File daemonConfigFile = new File(configDir, "daemon.conf"); - FileUtils.writeString( - daemonConfigFile, - String.join( - "\n", - "high-priority = yes", - "realtime-scheduling = no", - "flat-volumes = no", - "enable-deferred-volume = no", - "resample-method = speex-float-1", - "avoid-resampling = yes", - "default-sample-format = s16le", - "default-sample-rate = " + sampleRate, - "alternate-sample-rate = " + alternateSampleRate, - "default-sample-channels = " + options.channels, - "default-channel-map = " + channelMap, - "default-fragments = 4", - "default-fragment-size-msec = " + options.fragmentMillis, - "")); + boolean lowLatency = Options.PERFORMANCE_MODE_LOW_LATENCY.equals(options.performanceMode); + String sinkParams = "volume=" + options.volume + " performance_mode=1"; + if (lowLatency) sinkParams += " low_latency=true"; File configFile = new File(workingDir, "default.pa"); FileUtils.writeString( configFile, String.join( "\n", - "load-module module-native-protocol-unix auth-anonymous=1 auth-cookie-enabled=0 socket=\"" + "load-module module-native-protocol-unix auth-anonymous=1 auth-cookie-enabled=false socket=\"" + socketConfig.path + "\"", - "load-module module-aaudio-sink sink_name=AAudioSink rate=" + sampleRate, - "set-default-sink AAudioSink", - "set-sink-volume AAudioSink " + pulseVolumeHex(options.volume), + "load-module module-aaudio-sink " + sinkParams, "")); - String archName = AppUtils.getArchName(); - File modulesDir = new File(workingDir, "modules/" + archName); - patchAAudioSinkPerformanceMode(modulesDir); - String systemLibPath = archName.equals("arm64") ? "/system/lib64" : "/system/lib"; + File modulesDir = new File(workingDir, "modules"); ArrayList envVars = new ArrayList<>(); - envVars.add( - "LD_LIBRARY_PATH=" + systemLibPath + ":" + modulesDir + ":" + workingDir.getAbsolutePath()); + envVars.add("LD_LIBRARY_PATH=/system/lib64:" + nativeLibraryDir + ":" + modulesDir); envVars.add("HOME=" + workingDir); - envVars.add("XDG_CONFIG_HOME=" + new File(workingDir, ".config").getAbsolutePath()); - envVars.add("PULSE_RUNTIME_PATH=" + runtimeDir.getAbsolutePath()); - envVars.add("PULSE_LATENCY_MSEC=" + options.latencyMillis); envVars.add("TMPDIR=" + environment.getTmpDir()); - copyFromLibraryDir(workingDir); - - String command = workingDir.getAbsolutePath() + "/libpulseaudio.so"; + String command = nativeLibraryDir + "/libpulseaudio.so"; command += " --system=false"; command += " --disable-shm=true"; command += " --fail=false"; command += " -n --file=default.pa"; - command += " --daemonize=false"; + command += " --daemonize=true"; command += " --use-pid-file=false"; command += " --exit-idle-time=-1"; - command += " --high-priority=true"; - command += " --realtime=false"; - command += " --resample-method=speex-float-1"; - return ProcessHelper.exec(command, envVars.toArray(new String[0]), workingDir); + ProcessHelper.exec(command, envVars.toArray(new String[0]), workingDir); } - private static String pulseVolumeHex(float linearVolume) { - int pulseVolume = Math.max(0, Math.round(0x10000 * linearVolume)); - return "0x" + Integer.toHexString(pulseVolume); - } + private String execPactlCommand(String command) { + Context context = environment.getContext(); + String nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir; + File workingDir = new File(context.getFilesDir(), "/pulseaudio"); + if (!workingDir.isDirectory()) return ""; - private static int getNativeOutputSampleRate(Context context) { - try { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - String value = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); - if (value != null && !value.isEmpty()) return Math.max(8000, Integer.parseInt(value)); - } catch (Exception ignored) { + File pactl = new File(workingDir, "pactl"); + if (!pactl.isFile()) { + Log.w(TAG, "pactl not found at " + pactl.getAbsolutePath()); + return ""; } - return Options.DEFAULT_SAMPLE_RATE; - } - - private static int getAlternateSampleRate(int sampleRate) { - return sampleRate == Options.DEFAULT_ALTERNATE_SAMPLE_RATE - ? Options.DEFAULT_SAMPLE_RATE - : Options.DEFAULT_ALTERNATE_SAMPLE_RATE; - } - - private static String getChannelMap(int channels) { - return channels <= 1 ? "mono" : "front-left,front-right"; - } - - private void patchAAudioSinkPerformanceMode(File modulesDir) { - File module = new File(modulesDir, "module-aaudio-sink.so"); - if (!module.isFile()) return; - - int mode = 10; - if (Options.PERFORMANCE_MODE_POWER_SAVING.equals(options.performanceMode)) mode = 11; - else if (Options.PERFORMANCE_MODE_LOW_LATENCY.equals(options.performanceMode)) mode = 12; - - byte[][] searchPatterns = { - {0x41, 0x01, (byte) 0x80, 0x52}, - {0x61, 0x01, (byte) 0x80, 0x52}, - {(byte) 0x81, 0x01, (byte) 0x80, 0x52}, - {0x0a, 0x10, (byte) 0xa0, (byte) 0xe3}, - {0x0b, 0x10, (byte) 0xa0, (byte) 0xe3}, - {0x0c, 0x10, (byte) 0xa0, (byte) 0xe3} - }; - byte[] arm64Replacement = {(byte) (0x01 | (mode << 5)), 0x01, (byte) 0x80, 0x52}; - byte[] armhfReplacement = {(byte) mode, 0x10, (byte) 0xa0, (byte) 0xe3}; + if (!pactl.canExecute()) FileUtils.chmod(pactl, 0755); + File modulesDir = new File(workingDir, "modules"); + StringBuilder output = new StringBuilder(); try { - byte[] data = Files.readAllBytes(module.toPath()); - if (data.length < 4 - || data[0] != 0x7F - || data[1] != 'E' - || data[2] != 'L' - || data[3] != 'F') return; - boolean changed = false; - for (byte[] searchPattern : searchPatterns) { - int offset = findPattern(data, searchPattern, 0); - if (offset < 0) continue; - if (findPattern(data, searchPattern, offset + 1) >= 0) continue; - byte[] replacement = searchPattern[2] == (byte) 0x80 ? arm64Replacement : armhfReplacement; - for (int j = 0; j < replacement.length; j++) { - data[offset + j] = replacement[j]; - } - changed = true; - break; - } - if (changed) Files.write(module.toPath(), data); - } catch (IOException ignored) { - } - } - - private static int findPattern(byte[] data, byte[] pattern, int fromIndex) { - for (int i = Math.max(0, fromIndex); i <= data.length - pattern.length; i++) { - boolean found = true; - for (int j = 0; j < pattern.length; j++) { - if (data[i + j] != pattern[j]) { - found = false; - break; + String[] envp = + new String[] { + "LD_LIBRARY_PATH=/system/lib64:" + nativeLibraryDir + ":" + modulesDir, + "HOME=" + workingDir, + "PULSE_SERVER=" + socketConfig.path, + "TMPDIR=" + environment.getTmpDir() + }; + Process process = + Runtime.getRuntime().exec(pactl.getAbsolutePath() + " " + command, envp, workingDir); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append('\n'); } } - if (found) return i; + process.waitFor(); + } catch (IOException e) { + return ""; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return ""; } - return -1; + return output.toString(); } }