[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"