Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[camera] Fix IllegalStateException being thrown in Android implementation when switching activities. #4319

Merged
merged 6 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Integer> supportedImageFormats;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
*
* <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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -73,8 +70,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
binding.getActivity(),
flutterPluginBinding.getBinaryMessenger(),
binding::addRequestPermissionsResultListener,
flutterPluginBinding.getTextureRegistry(),
FlutterLifecycleAdapter.getActivityLifecycle(binding));
flutterPluginBinding.getTextureRegistry());
}

@Override
Expand All @@ -100,20 +96,14 @@ 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;
}

methodCallHandler =
new MethodCallHandlerImpl(
activity,
messenger,
new CameraPermissions(),
permissionsRegistry,
textureRegistry,
lifecycle);
activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,30 +27,27 @@
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;
private final PermissionsRegistry permissionsRegistry;
private final TextureRegistry textureRegistry;
private final MethodChannel methodChannel;
private final EventChannel imageStreamChannel;
private final Lifecycle lifecycle;
private @Nullable Camera camera;

MethodCallHandlerImpl(
Activity activity,
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");
Expand Down Expand Up @@ -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,
Expand All @@ -401,10 +392,6 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce
resolutionPreset,
enableAudio);

if (lifecycle != null) {
lifecycle.addObserver(camera);
}

Map<String, Object> reply = new HashMap<>();
reply.put("cameraId", flutterSurfaceTexture.id());
result.success(reply);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -57,6 +63,10 @@ public class CameraTest {
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() {
Expand All @@ -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 =
Expand All @@ -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(
Expand All @@ -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<Camera> cameraClass = Camera.class;

assertFalse(LifecycleObserver.class.isAssignableFrom(cameraClass));
}

@Test
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

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;
import static org.mockito.Mockito.verify;

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;
Expand All @@ -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<MethodCallHandlerImpl> methodCallHandlerClass = MethodCallHandlerImpl.class;

assertFalse(LifecycleObserver.class.isAssignableFrom(methodCallHandlerClass));
}

@Test
public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult()
throws CameraAccessException {
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down