From 9ef8644e9d0e8f71a4feb0c4bd991ea1e939964d Mon Sep 17 00:00:00 2001 From: Raju M <24muliyashiya@gmail.com> Date: Thu, 27 Nov 2025 14:10:57 +0530 Subject: [PATCH 1/2] [camera_android_camerax] Fixes crash on hot restart caused by stale observers --- .../camera_android_camerax/CHANGELOG.md | 4 ++++ .../plugins/camerax/ObserverProxyApi.java | 11 +++++++++ .../flutter/plugins/camerax/ObserverTest.java | 24 ++++++++++++++++++- .../camera_android_camerax/pubspec.yaml | 2 +- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 256fb9b78c7..5293910342e 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.25+1 + +* Fixes crash on hot restart caused by stale observers. + ## 0.6.25 * Adds support for `MediaSettings.fps` for camera preview, image streaming, and video recording. diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverProxyApi.java index 7318091be56..a677ad6118e 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverProxyApi.java @@ -29,6 +29,17 @@ public void onChanged(T t) { new ProxyApiRegistrar.FlutterMethodRunnable() { @Override public void run() { + // Check if this observer instance is still in the instance manager. + // During hot restart, old observers may remain attached to LiveData but + // are no longer tracked in the instance manager. + if (!api.getPigeonRegistrar().getInstanceManager().containsInstance(ObserverImpl.this)) { + android.util.Log.w( + "ObserverProxyApi", + "Ignoring onChanged callback for Observer not in InstanceManager (likely from previous hot restart): " + + ObserverImpl.this); + return; + } + api.onChanged( ObserverImpl.this, t, diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java index b0ca542e5f3..89d4edd4a9f 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -16,13 +17,34 @@ public class ObserverTest { @Test public void onChanged_makesExpectedCallToDartCallback() { final ObserverProxyApi mockApi = mock(ObserverProxyApi.class); - when(mockApi.getPigeonRegistrar()).thenReturn(new TestProxyApiRegistrar()); + final TestProxyApiRegistrar registrar = new TestProxyApiRegistrar(); + when(mockApi.getPigeonRegistrar()).thenReturn(registrar); final ObserverProxyApi.ObserverImpl instance = new ObserverProxyApi.ObserverImpl<>(mockApi); + + // Add the observer to the instance manager to simulate normal operation + registrar.getInstanceManager().addDartCreatedInstance(instance, 0); + final String value = "result"; instance.onChanged(value); verify(mockApi).onChanged(eq(instance), eq(value), any()); } + + @Test + public void onChanged_doesNotCallDartCallbackWhenObserverNotInInstanceManager() { + final ObserverProxyApi mockApi = mock(ObserverProxyApi.class); + final TestProxyApiRegistrar registrar = new TestProxyApiRegistrar(); + when(mockApi.getPigeonRegistrar()).thenReturn(registrar); + + final ObserverProxyApi.ObserverImpl instance = + new ObserverProxyApi.ObserverImpl<>(mockApi); + + final String value = "result"; + instance.onChanged(value); + + // Verify that the Dart callback is NOT invoked for stale observers + verify(mockApi, never()).onChanged(any(), any(), any()); + } } diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 756c856eba0..ca527345509 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.6.25 +version: 0.6.25+1 environment: sdk: ^3.9.0 From dc6ebe92d7592c73064f1e6687041e2d59c486b3 Mon Sep 17 00:00:00 2001 From: Raju M <24muliyashiya@gmail.com> Date: Thu, 27 Nov 2025 14:52:30 +0530 Subject: [PATCH 2/2] Code format and address gemini comment --- .../plugins/camerax/ObserverProxyApi.java | 4 ++- .../flutter/plugins/camerax/ObserverTest.java | 27 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverProxyApi.java index a677ad6118e..862886c7ac0 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ObserverProxyApi.java @@ -32,7 +32,9 @@ public void run() { // Check if this observer instance is still in the instance manager. // During hot restart, old observers may remain attached to LiveData but // are no longer tracked in the instance manager. - if (!api.getPigeonRegistrar().getInstanceManager().containsInstance(ObserverImpl.this)) { + if (!api.getPigeonRegistrar() + .getInstanceManager() + .containsInstance(ObserverImpl.this)) { android.util.Log.w( "ObserverProxyApi", "Ignoring onChanged callback for Observer not in InstanceManager (likely from previous hot restart): " diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java index 89d4edd4a9f..854f1e384cc 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ObserverTest.java @@ -11,18 +11,24 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import org.junit.Before; import org.junit.Test; public class ObserverTest { - @Test - public void onChanged_makesExpectedCallToDartCallback() { - final ObserverProxyApi mockApi = mock(ObserverProxyApi.class); - final TestProxyApiRegistrar registrar = new TestProxyApiRegistrar(); + private ObserverProxyApi mockApi; + private TestProxyApiRegistrar registrar; + private ObserverProxyApi.ObserverImpl instance; + + @Before + public void setUp() { + mockApi = mock(ObserverProxyApi.class); + registrar = new TestProxyApiRegistrar(); when(mockApi.getPigeonRegistrar()).thenReturn(registrar); + instance = new ObserverProxyApi.ObserverImpl<>(mockApi); + } - final ObserverProxyApi.ObserverImpl instance = - new ObserverProxyApi.ObserverImpl<>(mockApi); - + @Test + public void onChanged_makesExpectedCallToDartCallback() { // Add the observer to the instance manager to simulate normal operation registrar.getInstanceManager().addDartCreatedInstance(instance, 0); @@ -34,13 +40,6 @@ public void onChanged_makesExpectedCallToDartCallback() { @Test public void onChanged_doesNotCallDartCallbackWhenObserverNotInInstanceManager() { - final ObserverProxyApi mockApi = mock(ObserverProxyApi.class); - final TestProxyApiRegistrar registrar = new TestProxyApiRegistrar(); - when(mockApi.getPigeonRegistrar()).thenReturn(registrar); - - final ObserverProxyApi.ObserverImpl instance = - new ObserverProxyApi.ObserverImpl<>(mockApi); - final String value = "result"; instance.onChanged(value);