diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 62b5f1f9bd4c..b2dda9a52436 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 61d13e5579cc..633efd0b284a 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -60,7 +60,7 @@ android { 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 4601e7d34d69..75ced531b08a 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 @@ interface ErrorCallback { class Camera implements CameraCaptureCallback.CameraCaptureStateListener, - ImageReader.OnImageAvailableListener, - LifecycleObserver { + ImageReader.OnImageAvailableListener { private static final String TAG = "Camera"; private static final HashMap supportedImageFormats; @@ -576,19 +573,21 @@ private Display getDefaultDisplay() { } /** 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 @@ public void dispose() { 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. + * + *

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

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 ef3a2b9b5d83..067ed0295e2e 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 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.activity(), registrar.messenger(), registrar::addRequestPermissionsResultListener, - registrar.view(), - null); + registrar.view()); } @Override @@ -73,8 +70,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { binding.getActivity(), flutterPluginBinding.getBinaryMessenger(), binding::addRequestPermissionsResultListener, - flutterPluginBinding.getTextureRegistry(), - FlutterLifecycleAdapter.getActivityLifecycle(binding)); + flutterPluginBinding.getTextureRegistry()); } @Override @@ -100,8 +96,7 @@ private void maybeStartListening( 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 @@ private void maybeStartListening( 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 5e25353cbca9..35cc2b081bae 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 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, Li 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 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, Li 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 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce 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 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce resolutionPreset, enableAudio); - if (lifecycle != null) { - lifecycle.addObserver(camera); - } - Map 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 fbed28bc11fc..9d973195435e 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 @@ public class CameraTest { private Camera camera; private CameraCaptureSession mockCaptureSession; private CaptureRequest.Builder mockPreviewRequestBuilder; + private MockedStatic mockHandlerThreadFactory; + private HandlerThread mockHandlerThread; + private MockedStatic mockHandlerFactory; + private Handler mockHandler; @Before public void before() { @@ -65,6 +75,10 @@ public void before() { 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 @@ public void before() { 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 @@ public void before() { @After public void after() { TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0); + mockHandlerThreadFactory.close(); + mockHandlerFactory.close(); + } + + @Test + public void shouldNotImplementLifecycleObserverInterface() { + Class cameraClass = Camera.class; + + assertFalse(LifecycleObserver.class.isAssignableFrom(cameraClass)); } @Test @@ -773,6 +800,22 @@ public void resumePreview_shouldSendErrorEventOnCameraAccessException() 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 35eed7a66a1a..868e2e9e6d57 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,13 +35,19 @@ public void setUp() { 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 methodCallHandlerClass = MethodCallHandlerImpl.class; + + assertFalse(LifecycleObserver.class.isAssignableFrom(methodCallHandlerClass)); + } + @Test public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult() throws CameraAccessException { diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index b8894d58ac3a..5c225eaee48f 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the 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"