Skip to content

Commit 8a71e0e

Browse files
authored
[camera] Fix IllegalStateException being thrown in Android implementation when switching activities. (flutter#4319)
1 parent f58ab59 commit 8a71e0e

File tree

8 files changed

+107
-42
lines changed

8 files changed

+107
-42
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.4+1
2+
3+
* Fixed Android implementation throwing IllegalStateException when switching to a different activity.
4+
15
## 0.9.4
26

37
* Add web support by endorsing `package:camera_web`.

packages/camera/camera/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ android {
6060
dependencies {
6161
compileOnly 'androidx.annotation:annotation:1.1.0'
6262
testImplementation 'junit:junit:4.12'
63-
testImplementation 'org.mockito:mockito-inline:3.11.1'
63+
testImplementation 'org.mockito:mockito-inline:3.12.4'
6464
testImplementation 'androidx.test:core:1.3.0'
6565
testImplementation 'org.robolectric:robolectric:4.3'
6666
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@
3535
import android.view.Surface;
3636
import androidx.annotation.NonNull;
3737
import androidx.annotation.Nullable;
38-
import androidx.lifecycle.Lifecycle;
39-
import androidx.lifecycle.LifecycleObserver;
40-
import androidx.lifecycle.OnLifecycleEvent;
38+
import androidx.annotation.VisibleForTesting;
4139
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
4240
import io.flutter.plugin.common.EventChannel;
4341
import io.flutter.plugin.common.MethodChannel;
@@ -82,8 +80,7 @@ interface ErrorCallback {
8280

8381
class Camera
8482
implements CameraCaptureCallback.CameraCaptureStateListener,
85-
ImageReader.OnImageAvailableListener,
86-
LifecycleObserver {
83+
ImageReader.OnImageAvailableListener {
8784
private static final String TAG = "Camera";
8885

8986
private static final HashMap<String, Integer> supportedImageFormats;
@@ -576,19 +573,21 @@ private Display getDefaultDisplay() {
576573
}
577574

578575
/** Starts a background thread and its {@link Handler}. */
579-
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
580576
public void startBackgroundThread() {
581-
backgroundHandlerThread = new HandlerThread("CameraBackground");
577+
if (backgroundHandlerThread != null) {
578+
return;
579+
}
580+
581+
backgroundHandlerThread = HandlerThreadFactory.create("CameraBackground");
582582
try {
583583
backgroundHandlerThread.start();
584584
} catch (IllegalThreadStateException e) {
585585
// Ignore exception in case the thread has already started.
586586
}
587-
backgroundHandler = new Handler(backgroundHandlerThread.getLooper());
587+
backgroundHandler = HandlerFactory.create(backgroundHandlerThread.getLooper());
588588
}
589589

590590
/** Stops the background thread and its {@link Handler}. */
591-
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
592591
public void stopBackgroundThread() {
593592
if (backgroundHandlerThread != null) {
594593
backgroundHandlerThread.quitSafely();
@@ -1120,4 +1119,38 @@ public void dispose() {
11201119
flutterTexture.release();
11211120
getDeviceOrientationManager().stop();
11221121
}
1122+
1123+
/** Factory class that assists in creating a {@link HandlerThread} instance. */
1124+
static class HandlerThreadFactory {
1125+
/**
1126+
* Creates a new instance of the {@link HandlerThread} class.
1127+
*
1128+
* <p>This method is visible for testing purposes only and should never be used outside this *
1129+
* class.
1130+
*
1131+
* @param name to give to the HandlerThread.
1132+
* @return new instance of the {@link HandlerThread} class.
1133+
*/
1134+
@VisibleForTesting
1135+
public static HandlerThread create(String name) {
1136+
return new HandlerThread(name);
1137+
}
1138+
}
1139+
1140+
/** Factory class that assists in creating a {@link Handler} instance. */
1141+
static class HandlerFactory {
1142+
/**
1143+
* Creates a new instance of the {@link Handler} class.
1144+
*
1145+
* <p>This method is visible for testing purposes only and should never be used outside this *
1146+
* class.
1147+
*
1148+
* @param looper to give to the Handler.
1149+
* @return new instance of the {@link Handler} class.
1150+
*/
1151+
@VisibleForTesting
1152+
public static Handler create(Looper looper) {
1153+
return new Handler(looper);
1154+
}
1155+
}
11231156
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88
import android.os.Build;
99
import androidx.annotation.NonNull;
1010
import androidx.annotation.Nullable;
11-
import androidx.lifecycle.Lifecycle;
1211
import io.flutter.embedding.engine.plugins.FlutterPlugin;
1312
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
1413
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
15-
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
1614
import io.flutter.plugin.common.BinaryMessenger;
1715
import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
1816
import io.flutter.view.TextureRegistry;
@@ -53,8 +51,7 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra
5351
registrar.activity(),
5452
registrar.messenger(),
5553
registrar::addRequestPermissionsResultListener,
56-
registrar.view(),
57-
null);
54+
registrar.view());
5855
}
5956

6057
@Override
@@ -73,8 +70,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
7370
binding.getActivity(),
7471
flutterPluginBinding.getBinaryMessenger(),
7572
binding::addRequestPermissionsResultListener,
76-
flutterPluginBinding.getTextureRegistry(),
77-
FlutterLifecycleAdapter.getActivityLifecycle(binding));
73+
flutterPluginBinding.getTextureRegistry());
7874
}
7975

8076
@Override
@@ -100,20 +96,14 @@ private void maybeStartListening(
10096
Activity activity,
10197
BinaryMessenger messenger,
10298
PermissionsRegistry permissionsRegistry,
103-
TextureRegistry textureRegistry,
104-
@Nullable Lifecycle lifecycle) {
99+
TextureRegistry textureRegistry) {
105100
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
106101
// If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin.
107102
return;
108103
}
109104

110105
methodCallHandler =
111106
new MethodCallHandlerImpl(
112-
activity,
113-
messenger,
114-
new CameraPermissions(),
115-
permissionsRegistry,
116-
textureRegistry,
117-
lifecycle);
107+
activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry);
118108
}
119109
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import android.os.Looper;
1111
import androidx.annotation.NonNull;
1212
import androidx.annotation.Nullable;
13-
import androidx.lifecycle.Lifecycle;
14-
import androidx.lifecycle.LifecycleObserver;
1513
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
1614
import io.flutter.plugin.common.BinaryMessenger;
1715
import io.flutter.plugin.common.EventChannel;
@@ -29,30 +27,27 @@
2927
import java.util.HashMap;
3028
import java.util.Map;
3129

32-
final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, LifecycleObserver {
30+
final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
3331
private final Activity activity;
3432
private final BinaryMessenger messenger;
3533
private final CameraPermissions cameraPermissions;
3634
private final PermissionsRegistry permissionsRegistry;
3735
private final TextureRegistry textureRegistry;
3836
private final MethodChannel methodChannel;
3937
private final EventChannel imageStreamChannel;
40-
private final Lifecycle lifecycle;
4138
private @Nullable Camera camera;
4239

4340
MethodCallHandlerImpl(
4441
Activity activity,
4542
BinaryMessenger messenger,
4643
CameraPermissions cameraPermissions,
4744
PermissionsRegistry permissionsAdder,
48-
TextureRegistry textureRegistry,
49-
@Nullable Lifecycle lifecycle) {
45+
TextureRegistry textureRegistry) {
5046
this.activity = activity;
5147
this.messenger = messenger;
5248
this.cameraPermissions = cameraPermissions;
5349
this.permissionsRegistry = permissionsAdder;
5450
this.textureRegistry = textureRegistry;
55-
this.lifecycle = lifecycle;
5651

5752
methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera");
5853
imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream");
@@ -387,10 +382,6 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce
387382
new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity));
388383
ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset);
389384

390-
if (camera != null && lifecycle != null) {
391-
lifecycle.removeObserver(camera);
392-
}
393-
394385
camera =
395386
new Camera(
396387
activity,
@@ -401,10 +392,6 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce
401392
resolutionPreset,
402393
enableAudio);
403394

404-
if (lifecycle != null) {
405-
lifecycle.addObserver(camera);
406-
}
407-
408395
Map<String, Object> reply = new HashMap<>();
409396
reply.put("cameraId", flutterSurfaceTexture.id());
410397
result.success(reply);

packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
package io.flutter.plugins.camera;
66

77
import static org.junit.Assert.assertEquals;
8+
import static org.junit.Assert.assertFalse;
89
import static org.junit.Assert.assertNotNull;
910
import static org.mockito.ArgumentMatchers.any;
1011
import static org.mockito.ArgumentMatchers.eq;
1112
import static org.mockito.Mockito.doThrow;
1213
import static org.mockito.Mockito.mock;
14+
import static org.mockito.Mockito.mockStatic;
1315
import static org.mockito.Mockito.never;
1416
import static org.mockito.Mockito.times;
1517
import static org.mockito.Mockito.verify;
@@ -23,7 +25,10 @@
2325
import android.media.CamcorderProfile;
2426
import android.media.MediaRecorder;
2527
import android.os.Build;
28+
import android.os.Handler;
29+
import android.os.HandlerThread;
2630
import androidx.annotation.NonNull;
31+
import androidx.lifecycle.LifecycleObserver;
2732
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
2833
import io.flutter.plugin.common.MethodChannel;
2934
import io.flutter.plugins.camera.features.CameraFeatureFactory;
@@ -49,6 +54,7 @@
4954
import org.junit.After;
5055
import org.junit.Before;
5156
import org.junit.Test;
57+
import org.mockito.MockedStatic;
5258

5359
public class CameraTest {
5460
private CameraProperties mockCameraProperties;
@@ -57,6 +63,10 @@ public class CameraTest {
5763
private Camera camera;
5864
private CameraCaptureSession mockCaptureSession;
5965
private CaptureRequest.Builder mockPreviewRequestBuilder;
66+
private MockedStatic<Camera.HandlerThreadFactory> mockHandlerThreadFactory;
67+
private HandlerThread mockHandlerThread;
68+
private MockedStatic<Camera.HandlerFactory> mockHandlerFactory;
69+
private Handler mockHandler;
6070

6171
@Before
6272
public void before() {
@@ -65,6 +75,10 @@ public void before() {
6575
mockDartMessenger = mock(DartMessenger.class);
6676
mockCaptureSession = mock(CameraCaptureSession.class);
6777
mockPreviewRequestBuilder = mock(CaptureRequest.Builder.class);
78+
mockHandlerThreadFactory = mockStatic(Camera.HandlerThreadFactory.class);
79+
mockHandlerThread = mock(HandlerThread.class);
80+
mockHandlerFactory = mockStatic(Camera.HandlerFactory.class);
81+
mockHandler = mock(Handler.class);
6882

6983
final Activity mockActivity = mock(Activity.class);
7084
final TextureRegistry.SurfaceTextureEntry mockFlutterTexture =
@@ -74,6 +88,10 @@ public void before() {
7488
final boolean enableAudio = false;
7589

7690
when(mockCameraProperties.getCameraName()).thenReturn(cameraName);
91+
mockHandlerFactory.when(() -> Camera.HandlerFactory.create(any())).thenReturn(mockHandler);
92+
mockHandlerThreadFactory
93+
.when(() -> Camera.HandlerThreadFactory.create(any()))
94+
.thenReturn(mockHandlerThread);
7795

7896
camera =
7997
new Camera(
@@ -92,6 +110,15 @@ public void before() {
92110
@After
93111
public void after() {
94112
TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0);
113+
mockHandlerThreadFactory.close();
114+
mockHandlerFactory.close();
115+
}
116+
117+
@Test
118+
public void shouldNotImplementLifecycleObserverInterface() {
119+
Class<Camera> cameraClass = Camera.class;
120+
121+
assertFalse(LifecycleObserver.class.isAssignableFrom(cameraClass));
95122
}
96123

97124
@Test
@@ -773,6 +800,22 @@ public void resumePreview_shouldSendErrorEventOnCameraAccessException()
773800
verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any());
774801
}
775802

803+
@Test
804+
public void startBackgroundThread_shouldStartNewThread() {
805+
camera.startBackgroundThread();
806+
807+
verify(mockHandlerThread, times(1)).start();
808+
assertEquals(mockHandler, TestUtils.getPrivateField(camera, "backgroundHandler"));
809+
}
810+
811+
@Test
812+
public void startBackgroundThread_shouldNotStartNewThreadWhenAlreadyCreated() {
813+
camera.startBackgroundThread();
814+
camera.startBackgroundThread();
815+
816+
verify(mockHandlerThread, times(1)).start();
817+
}
818+
776819
private static class TestCameraFeatureFactory implements CameraFeatureFactory {
777820
private final AutoFocusFeature mockAutoFocusFeature;
778821
private final ExposureLockFeature mockExposureLockFeature;

packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
package io.flutter.plugins.camera;
66

7+
import static org.junit.Assert.assertFalse;
78
import static org.mockito.Mockito.doThrow;
89
import static org.mockito.Mockito.mock;
910
import static org.mockito.Mockito.times;
1011
import static org.mockito.Mockito.verify;
1112

1213
import android.app.Activity;
1314
import android.hardware.camera2.CameraAccessException;
15+
import androidx.lifecycle.LifecycleObserver;
1416
import io.flutter.plugin.common.BinaryMessenger;
1517
import io.flutter.plugin.common.MethodCall;
1618
import io.flutter.plugin.common.MethodChannel;
@@ -33,13 +35,19 @@ public void setUp() {
3335
mock(BinaryMessenger.class),
3436
mock(CameraPermissions.class),
3537
mock(CameraPermissions.PermissionsRegistry.class),
36-
mock(TextureRegistry.class),
37-
null);
38+
mock(TextureRegistry.class));
3839
mockResult = mock(MethodChannel.Result.class);
3940
mockCamera = mock(Camera.class);
4041
TestUtils.setPrivateField(handler, "camera", mockCamera);
4142
}
4243

44+
@Test
45+
public void shouldNotImplementLifecycleObserverInterface() {
46+
Class<MethodCallHandlerImpl> methodCallHandlerClass = MethodCallHandlerImpl.class;
47+
48+
assertFalse(LifecycleObserver.class.isAssignableFrom(methodCallHandlerClass));
49+
}
50+
4351
@Test
4452
public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult()
4553
throws CameraAccessException {

packages/camera/camera/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the
44
and streaming image buffers to dart.
55
repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
7-
version: 0.9.4
7+
version: 0.9.4+1
88

99
environment:
1010
sdk: ">=2.14.0 <3.0.0"

0 commit comments

Comments
 (0)