[camera] Fix IllegalStateException being thrown in Android implementation when switching activities. (#4319)
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index 62b5f1f..b2dda9a 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.4+1
+
+* Fixed Android implementation throwing IllegalStateException when switching to a different activity.
+
## 0.9.4
* Add web support by endorsing `package:camera_web`.
diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle
index 61d13e5..633efd0 100644
--- a/packages/camera/camera/android/build.gradle
+++ b/packages/camera/camera/android/build.gradle
@@ -60,7 +60,7 @@
dependencies {
compileOnly 'androidx.annotation:annotation:1.1.0'
testImplementation 'junit:junit:4.12'
- testImplementation 'org.mockito:mockito-inline:3.11.1'
+ testImplementation 'org.mockito:mockito-inline:3.12.4'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.robolectric:robolectric:4.3'
}
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
index 4601e7d..75ced53 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
@@ -35,9 +35,7 @@
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleObserver;
-import androidx.lifecycle.OnLifecycleEvent;
+import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel;
@@ -82,8 +80,7 @@
class Camera
implements CameraCaptureCallback.CameraCaptureStateListener,
- ImageReader.OnImageAvailableListener,
- LifecycleObserver {
+ ImageReader.OnImageAvailableListener {
private static final String TAG = "Camera";
private static final HashMap<String, Integer> supportedImageFormats;
@@ -576,19 +573,21 @@
}
/** Starts a background thread and its {@link Handler}. */
- @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void startBackgroundThread() {
- backgroundHandlerThread = new HandlerThread("CameraBackground");
+ if (backgroundHandlerThread != null) {
+ return;
+ }
+
+ backgroundHandlerThread = HandlerThreadFactory.create("CameraBackground");
try {
backgroundHandlerThread.start();
} catch (IllegalThreadStateException e) {
// Ignore exception in case the thread has already started.
}
- backgroundHandler = new Handler(backgroundHandlerThread.getLooper());
+ backgroundHandler = HandlerFactory.create(backgroundHandlerThread.getLooper());
}
/** Stops the background thread and its {@link Handler}. */
- @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void stopBackgroundThread() {
if (backgroundHandlerThread != null) {
backgroundHandlerThread.quitSafely();
@@ -1120,4 +1119,38 @@
flutterTexture.release();
getDeviceOrientationManager().stop();
}
+
+ /** Factory class that assists in creating a {@link HandlerThread} instance. */
+ static class HandlerThreadFactory {
+ /**
+ * Creates a new instance of the {@link HandlerThread} class.
+ *
+ * <p>This method is visible for testing purposes only and should never be used outside this *
+ * class.
+ *
+ * @param name to give to the HandlerThread.
+ * @return new instance of the {@link HandlerThread} class.
+ */
+ @VisibleForTesting
+ public static HandlerThread create(String name) {
+ return new HandlerThread(name);
+ }
+ }
+
+ /** Factory class that assists in creating a {@link Handler} instance. */
+ static class HandlerFactory {
+ /**
+ * Creates a new instance of the {@link Handler} class.
+ *
+ * <p>This method is visible for testing purposes only and should never be used outside this *
+ * class.
+ *
+ * @param looper to give to the Handler.
+ * @return new instance of the {@link Handler} class.
+ */
+ @VisibleForTesting
+ public static Handler create(Looper looper) {
+ return new Handler(looper);
+ }
+ }
}
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java
index ef3a2b9..067ed02 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java
@@ -8,11 +8,9 @@
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.lifecycle.Lifecycle;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
-import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
import io.flutter.view.TextureRegistry;
@@ -53,8 +51,7 @@
registrar.activity(),
registrar.messenger(),
registrar::addRequestPermissionsResultListener,
- registrar.view(),
- null);
+ registrar.view());
}
@Override
@@ -73,8 +70,7 @@
binding.getActivity(),
flutterPluginBinding.getBinaryMessenger(),
binding::addRequestPermissionsResultListener,
- flutterPluginBinding.getTextureRegistry(),
- FlutterLifecycleAdapter.getActivityLifecycle(binding));
+ flutterPluginBinding.getTextureRegistry());
}
@Override
@@ -100,8 +96,7 @@
Activity activity,
BinaryMessenger messenger,
PermissionsRegistry permissionsRegistry,
- TextureRegistry textureRegistry,
- @Nullable Lifecycle lifecycle) {
+ TextureRegistry textureRegistry) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin.
return;
@@ -109,11 +104,6 @@
methodCallHandler =
new MethodCallHandlerImpl(
- activity,
- messenger,
- new CameraPermissions(),
- permissionsRegistry,
- textureRegistry,
- lifecycle);
+ activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry);
}
}
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
index 5e25353..35cc2b0 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
@@ -10,8 +10,6 @@
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleObserver;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
@@ -29,7 +27,7 @@
import java.util.HashMap;
import java.util.Map;
-final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, LifecycleObserver {
+final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
private final Activity activity;
private final BinaryMessenger messenger;
private final CameraPermissions cameraPermissions;
@@ -37,7 +35,6 @@
private final TextureRegistry textureRegistry;
private final MethodChannel methodChannel;
private final EventChannel imageStreamChannel;
- private final Lifecycle lifecycle;
private @Nullable Camera camera;
MethodCallHandlerImpl(
@@ -45,14 +42,12 @@
BinaryMessenger messenger,
CameraPermissions cameraPermissions,
PermissionsRegistry permissionsAdder,
- TextureRegistry textureRegistry,
- @Nullable Lifecycle lifecycle) {
+ TextureRegistry textureRegistry) {
this.activity = activity;
this.messenger = messenger;
this.cameraPermissions = cameraPermissions;
this.permissionsRegistry = permissionsAdder;
this.textureRegistry = textureRegistry;
- this.lifecycle = lifecycle;
methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera");
imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream");
@@ -387,10 +382,6 @@
new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity));
ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset);
- if (camera != null && lifecycle != null) {
- lifecycle.removeObserver(camera);
- }
-
camera =
new Camera(
activity,
@@ -401,10 +392,6 @@
resolutionPreset,
enableAudio);
- if (lifecycle != null) {
- lifecycle.addObserver(camera);
- }
-
Map<String, Object> reply = new HashMap<>();
reply.put("cameraId", flutterSurfaceTexture.id());
result.success(reply);
diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
index fbed28b..9d97319 100644
--- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
+++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
@@ -5,11 +5,13 @@
package io.flutter.plugins.camera;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -23,7 +25,10 @@
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleObserver;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.camera.features.CameraFeatureFactory;
@@ -49,6 +54,7 @@
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.MockedStatic;
public class CameraTest {
private CameraProperties mockCameraProperties;
@@ -57,6 +63,10 @@
private Camera camera;
private CameraCaptureSession mockCaptureSession;
private CaptureRequest.Builder mockPreviewRequestBuilder;
+ private MockedStatic<Camera.HandlerThreadFactory> mockHandlerThreadFactory;
+ private HandlerThread mockHandlerThread;
+ private MockedStatic<Camera.HandlerFactory> mockHandlerFactory;
+ private Handler mockHandler;
@Before
public void before() {
@@ -65,6 +75,10 @@
mockDartMessenger = mock(DartMessenger.class);
mockCaptureSession = mock(CameraCaptureSession.class);
mockPreviewRequestBuilder = mock(CaptureRequest.Builder.class);
+ mockHandlerThreadFactory = mockStatic(Camera.HandlerThreadFactory.class);
+ mockHandlerThread = mock(HandlerThread.class);
+ mockHandlerFactory = mockStatic(Camera.HandlerFactory.class);
+ mockHandler = mock(Handler.class);
final Activity mockActivity = mock(Activity.class);
final TextureRegistry.SurfaceTextureEntry mockFlutterTexture =
@@ -74,6 +88,10 @@
final boolean enableAudio = false;
when(mockCameraProperties.getCameraName()).thenReturn(cameraName);
+ mockHandlerFactory.when(() -> Camera.HandlerFactory.create(any())).thenReturn(mockHandler);
+ mockHandlerThreadFactory
+ .when(() -> Camera.HandlerThreadFactory.create(any()))
+ .thenReturn(mockHandlerThread);
camera =
new Camera(
@@ -92,6 +110,15 @@
@After
public void after() {
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0);
+ mockHandlerThreadFactory.close();
+ mockHandlerFactory.close();
+ }
+
+ @Test
+ public void shouldNotImplementLifecycleObserverInterface() {
+ Class<Camera> cameraClass = Camera.class;
+
+ assertFalse(LifecycleObserver.class.isAssignableFrom(cameraClass));
}
@Test
@@ -773,6 +800,22 @@
verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any());
}
+ @Test
+ public void startBackgroundThread_shouldStartNewThread() {
+ camera.startBackgroundThread();
+
+ verify(mockHandlerThread, times(1)).start();
+ assertEquals(mockHandler, TestUtils.getPrivateField(camera, "backgroundHandler"));
+ }
+
+ @Test
+ public void startBackgroundThread_shouldNotStartNewThreadWhenAlreadyCreated() {
+ camera.startBackgroundThread();
+ camera.startBackgroundThread();
+
+ verify(mockHandlerThread, times(1)).start();
+ }
+
private static class TestCameraFeatureFactory implements CameraFeatureFactory {
private final AutoFocusFeature mockAutoFocusFeature;
private final ExposureLockFeature mockExposureLockFeature;
diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java
index 35eed7a..868e2e9 100644
--- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java
+++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java
@@ -4,6 +4,7 @@
package io.flutter.plugins.camera;
+import static org.junit.Assert.assertFalse;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -11,6 +12,7 @@
import android.app.Activity;
import android.hardware.camera2.CameraAccessException;
+import androidx.lifecycle.LifecycleObserver;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
@@ -33,14 +35,20 @@
mock(BinaryMessenger.class),
mock(CameraPermissions.class),
mock(CameraPermissions.PermissionsRegistry.class),
- mock(TextureRegistry.class),
- null);
+ mock(TextureRegistry.class));
mockResult = mock(MethodChannel.Result.class);
mockCamera = mock(Camera.class);
TestUtils.setPrivateField(handler, "camera", mockCamera);
}
@Test
+ public void shouldNotImplementLifecycleObserverInterface() {
+ Class<MethodCallHandlerImpl> methodCallHandlerClass = MethodCallHandlerImpl.class;
+
+ assertFalse(LifecycleObserver.class.isAssignableFrom(methodCallHandlerClass));
+ }
+
+ @Test
public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult()
throws CameraAccessException {
handler.onMethodCall(new MethodCall("pausePreview", null), mockResult);
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index b8894d5..5c225ea 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
and streaming image buffers to dart.
repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.4
+version: 0.9.4+1
environment:
sdk: ">=2.14.0 <3.0.0"