diff --git a/CHANGELOG.md b/CHANGELOG.md index 98458b1e..5dc6f142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### 4.0.13 (20 Jul 2026) +- Android: the plugin now does no native work at app startup. Plugin registration and `onAttachedToEngine` are pure Java bookkeeping; the native library is loaded lazily, only when an engine-lifecycle hook actually has to call into it. Previously a static initializer pulled the whole library onto the main thread during app launch even for apps that never played a sound, and a load failure there crashed plugin registration - fix: Waveform audio sources do not match engine sample rate #501. Thanks to @Colton127 - Android now stops the audio device when idle (no active voices) like every other platform, releasing the audioserver `AudioMix` partial wakelock #250; use `setAudioDeviceIdleTimeout()` to keep it running - add `stopAudioDevice()` / `startAudioDevice()` to control the audio output device without deinitializing the engine (loaded sounds and voice state are preserved); `stopAudioDevice()` is an idle-only no-op while an unpaused voice is active unless `force: true` is passed, while `startAudioDevice()` temporarily starts or prewarms the output and remains subject to the configured idle timeout; blocking native device operations run off the UI thread diff --git a/android/src/main/java/flutter/soloud/flutter_soloud/FlutterSoloudPlugin.java b/android/src/main/java/flutter/soloud/flutter_soloud/FlutterSoloudPlugin.java index 9484b3df..5f0d30fd 100644 --- a/android/src/main/java/flutter/soloud/flutter_soloud/FlutterSoloudPlugin.java +++ b/android/src/main/java/flutter/soloud/flutter_soloud/FlutterSoloudPlugin.java @@ -1,23 +1,59 @@ package flutter.soloud.flutter_soloud; +import android.util.Log; import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; public final class FlutterSoloudPlugin implements FlutterPlugin { - static { - System.loadLibrary("flutter_soloud_plugin"); - } + private static final String TAG = "FlutterSoloudPlugin"; + + /** + * Guarded by the class monitor. + * + *
The native library is loaded lazily, at the first point one of the + * hooks below actually needs to call into it -- never from a static + * initializer and never from {@link #onAttachedToEngine}. Both of those run + * at engine startup (GeneratedPluginRegistrant instantiates and attaches + * every plugin), which would drag the whole multi-megabyte library onto the + * main thread during app launch even for an app that never plays a sound. + * + *
By the time a hook fires, an app that uses SoLoud has already loaded
+ * the same library from Dart via {@code DynamicLibrary.open}, so
+ * {@code System.loadLibrary} is a refcount bump rather than a real load.
+ * Successful loads are cached; failed loads can be retried by a later hook.
+ */
+ private static boolean nativeLibraryLoaded = false;
private static native boolean
nativeClearDartCallbackRegistrationsForEngine(long engineId);
private Long engineId;
+ private static synchronized boolean ensureNativeLibraryLoaded() {
+ if (nativeLibraryLoaded) {
+ return true;
+ }
+
+ try {
+ System.loadLibrary("flutter_soloud_plugin");
+ nativeLibraryLoaded = true;
+ return true;
+ } catch (UnsatisfiedLinkError error) {
+ // Never fail engine teardown over this. The FFI layer opens the
+ // same library from Dart and surfaces the error there, where it is
+ // catchable and actionable.
+ return false;
+ }
+ }
+
@SuppressWarnings("deprecation")
@Override
public void onAttachedToEngine(
@NonNull FlutterPluginBinding binding
) {
+ // Deliberately does no native work. This runs during app launch for
+ // every app that depends on the plugin, whether or not it ever uses
+ // SoLoud, so it must stay pure Java bookkeeping.
engineId = binding.getFlutterEngine().getEngineId();
}
@@ -28,9 +64,17 @@ public void onDetachedFromEngine(
final Long detachedEngineId = engineId;
engineId = null;
- if (detachedEngineId != null) {
- nativeClearDartCallbackRegistrationsForEngine(
- detachedEngineId
+ if (detachedEngineId == null || !ensureNativeLibraryLoaded()) {
+ return;
+ }
+
+ try {
+ nativeClearDartCallbackRegistrationsForEngine(detachedEngineId);
+ } catch (UnsatisfiedLinkError error) {
+ Log.w(
+ TAG,
+ "Unable to clear Dart callback registrations during engine teardown",
+ error
);
}
}
diff --git a/example/tests/tests.dart b/example/tests/tests.dart
index 793fa758..e34a6d5d 100644
--- a/example/tests/tests.dart
+++ b/example/tests/tests.dart
@@ -56,19 +56,25 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State