diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index b34eb68980a0..6694f188beb4 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.0+27 + +* Removes or updates any references to an `ActivityPluginBinding` when the plugin is detached + or attached/re-attached, respectively, to an `Activity.` + ## 0.5.0+26 * Fixes new lint warnings. diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index c8046db5af6d..6781e85212ec 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -20,13 +20,15 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware { private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; - private PendingRecordingHostApiImpl pendingRecordingHostApiImpl; - private RecorderHostApiImpl recorderHostApiImpl; - private VideoCaptureHostApiImpl videoCaptureHostApiImpl; - private ImageAnalysisHostApiImpl imageAnalysisHostApiImpl; - private ImageCaptureHostApiImpl imageCaptureHostApiImpl; - private CameraControlHostApiImpl cameraControlHostApiImpl; - public @Nullable SystemServicesHostApiImpl systemServicesHostApiImpl; + @VisibleForTesting @Nullable public PendingRecordingHostApiImpl pendingRecordingHostApiImpl; + @VisibleForTesting @Nullable public RecorderHostApiImpl recorderHostApiImpl; + @VisibleForTesting @Nullable public VideoCaptureHostApiImpl videoCaptureHostApiImpl; + @VisibleForTesting @Nullable public ImageAnalysisHostApiImpl imageAnalysisHostApiImpl; + @VisibleForTesting @Nullable public ImageCaptureHostApiImpl imageCaptureHostApiImpl; + @VisibleForTesting @Nullable public CameraControlHostApiImpl cameraControlHostApiImpl; + @VisibleForTesting @Nullable public SystemServicesHostApiImpl systemServicesHostApiImpl; + + @VisibleForTesting public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl; @VisibleForTesting @@ -136,42 +138,54 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { Activity activity = activityPluginBinding.getActivity(); + // Set up Host API implementations based on the context that `activity` provides. setUp(pluginBinding.getBinaryMessenger(), activity, pluginBinding.getTextureRegistry()); - if (activity instanceof LifecycleOwner) { - processCameraProviderHostApiImpl.setLifecycleOwner((LifecycleOwner) activity); - liveDataHostApiImpl.setLifecycleOwner((LifecycleOwner) activity); - } else { - ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); - processCameraProviderHostApiImpl.setLifecycleOwner(proxyLifecycleProvider); - liveDataHostApiImpl.setLifecycleOwner(proxyLifecycleProvider); - } + // Set any needed references to `activity` itself. + updateLifecycleOwner(activity); + updateActivity(activity); - systemServicesHostApiImpl.setActivity(activity); + // Set permissions registry reference. systemServicesHostApiImpl.setPermissionsRegistry( activityPluginBinding::addRequestPermissionsResultListener); - deviceOrientationManagerHostApiImpl.setActivity(activity); } @Override public void onDetachedFromActivityForConfigChanges() { + // Clear any references to previously attached `ActivityPluginBinding`/`Activity`. updateContext(pluginBinding.getApplicationContext()); + updateLifecycleOwner(null); + updateActivity(null); + systemServicesHostApiImpl.setPermissionsRegistry(null); } @Override public void onReattachedToActivityForConfigChanges( @NonNull ActivityPluginBinding activityPluginBinding) { - updateContext(activityPluginBinding.getActivity()); + Activity activity = activityPluginBinding.getActivity(); + + // Set any needed references to `activity` itself or its context. + updateContext(activity); + updateLifecycleOwner(activity); + updateActivity(activity); + + // Set permissions registry reference. + systemServicesHostApiImpl.setPermissionsRegistry( + activityPluginBinding::addRequestPermissionsResultListener); } @Override public void onDetachedFromActivity() { + // Clear any references to previously attached `ActivityPluginBinding`/`Activity`. updateContext(pluginBinding.getApplicationContext()); + updateLifecycleOwner(null); + updateActivity(null); + systemServicesHostApiImpl.setPermissionsRegistry(null); } /** * Updates context that is used to fetch the corresponding instance of a {@code - * ProcessCameraProvider}. + * ProcessCameraProvider} to each of the relevant camera controls. */ public void updateContext(@NonNull Context context) { if (processCameraProviderHostApiImpl != null) { @@ -196,4 +210,32 @@ public void updateContext(@NonNull Context context) { cameraControlHostApiImpl.setContext(context); } } + + /** Sets {@code LifecycleOwner} that is used to control the lifecycle of the camera by CameraX. */ + public void updateLifecycleOwner(@Nullable Activity activity) { + if (activity == null) { + processCameraProviderHostApiImpl.setLifecycleOwner(null); + liveDataHostApiImpl.setLifecycleOwner(null); + } else if (activity instanceof LifecycleOwner) { + processCameraProviderHostApiImpl.setLifecycleOwner((LifecycleOwner) activity); + liveDataHostApiImpl.setLifecycleOwner((LifecycleOwner) activity); + } else { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + processCameraProviderHostApiImpl.setLifecycleOwner(proxyLifecycleProvider); + liveDataHostApiImpl.setLifecycleOwner(proxyLifecycleProvider); + } + } + + /** + * Updates {@code Activity} that is used for requesting camera permissions and tracking the + * orientation of the device. + */ + public void updateActivity(@Nullable Activity activity) { + if (systemServicesHostApiImpl != null) { + systemServicesHostApiImpl.setActivity(activity); + } + if (deviceOrientationManagerHostApiImpl != null) { + deviceOrientationManagerHostApiImpl.setActivity(activity); + } + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java index e70714a8bffe..2524d503af4b 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java @@ -36,6 +36,10 @@ public void enableTorch( @NonNull CameraControl cameraControl, @NonNull Boolean torch, @NonNull GeneratedCameraXLibrary.Result result) { + if (context == null) { + throw new IllegalStateException("Context must be set to enable the torch."); + } + ListenableFuture enableTorchFuture = cameraControl.enableTorch(torch); Futures.addCallback( @@ -58,6 +62,10 @@ public void setZoomRatio( @NonNull CameraControl cameraControl, @NonNull Double ratio, @NonNull GeneratedCameraXLibrary.Result result) { + if (context == null) { + throw new IllegalStateException("Context must be set to set zoom ratio."); + } + float ratioAsFloat = ratio.floatValue(); ListenableFuture setZoomRatioFuture = cameraControl.setZoomRatio(ratioAsFloat); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java index e617d53c99cd..22bba48e1bad 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java @@ -48,6 +48,11 @@ public void setActivity(@NonNull Activity activity) { @Override public void startListeningForDeviceOrientationChange( @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation) { + if (activity == null) { + throw new IllegalStateException( + "Activity must be set to start listening for device orientation changes."); + } + deviceOrientationManager = cameraXProxy.createDeviceOrientationManager( activity, diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java index f44db11cba1f..95b2cd46798d 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java @@ -19,7 +19,7 @@ public class ImageAnalysisHostApiImpl implements ImageAnalysisHostApi { private InstanceManager instanceManager; private BinaryMessenger binaryMessenger; - private Context context; + @Nullable private Context context; @VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy(); @@ -65,6 +65,10 @@ public void create( */ @Override public void setAnalyzer(@NonNull Long identifier, @NonNull Long analyzerIdentifier) { + if (context == null) { + throw new IllegalStateException("Context must be set to set an Analyzer."); + } + getImageAnalysisInstance(identifier) .setAnalyzer( ContextCompat.getMainExecutor(context), diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java index e17d386632f5..8e2e6f7291a5 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java @@ -22,7 +22,7 @@ public class ImageCaptureHostApiImpl implements ImageCaptureHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; - private Context context; + @Nullable private Context context; private SystemServicesFlutterApiImpl systemServicesFlutterApiImpl; public static final String TEMPORARY_FILE_NAME = "CAP"; @@ -87,6 +87,10 @@ public void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode) { @Override public void takePicture( @NonNull Long identifier, @NonNull GeneratedCameraXLibrary.Result result) { + if (context == null) { + throw new IllegalStateException("Context must be set to take picture."); + } + ImageCapture imageCapture = getImageCaptureInstance(identifier); final File outputDir = context.getCacheDir(); File temporaryCaptureFile; diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataHostApiImpl.java index 3e9e595d4b0f..3f7ec5125ec4 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/LiveDataHostApiImpl.java @@ -25,7 +25,7 @@ public class LiveDataHostApiImpl implements LiveDataHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; - private LifecycleOwner lifecycleOwner; + @Nullable private LifecycleOwner lifecycleOwner; /** * Constructs a {@link LiveDataHostApiImpl}. @@ -40,7 +40,7 @@ public LiveDataHostApiImpl( } /** Sets {@link LifecycleOwner} used to observe the camera state if so requested. */ - public void setLifecycleOwner(@NonNull LifecycleOwner lifecycleOwner) { + public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) { this.lifecycleOwner = lifecycleOwner; } @@ -51,6 +51,10 @@ public void setLifecycleOwner(@NonNull LifecycleOwner lifecycleOwner) { @Override @SuppressWarnings("unchecked") public void observe(@NonNull Long identifier, @NonNull Long observerIdentifier) { + if (lifecycleOwner == null) { + throw new IllegalStateException("LifecycleOwner must be set to observe a LiveData instance."); + } + getLiveDataInstance(identifier) .observe( lifecycleOwner, @@ -60,6 +64,10 @@ public void observe(@NonNull Long identifier, @NonNull Long observerIdentifier) /** Removes all observers of this instance that are tied to the {@link lifecycleOwner}. */ @Override public void removeObservers(@NonNull Long identifier) { + if (lifecycleOwner == null) { + throw new IllegalStateException("LifecycleOwner must be set to remove LiveData observers."); + } + getLiveDataInstance(identifier).removeObservers(lifecycleOwner); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java index d4acebefbffb..a1d661d1d9c1 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java @@ -20,7 +20,7 @@ public class PendingRecordingHostApiImpl implements PendingRecordingHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; - private Context context; + @Nullable private Context context; @VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy(); @@ -63,6 +63,10 @@ public Long start(@NonNull Long identifier) { @Nullable @VisibleForTesting public Executor getExecutor() { + if (context == null) { + throw new IllegalStateException("Context must be set to get an executor to start recording."); + } + return ContextCompat.getMainExecutor(context); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java index 9292ee93fc43..0e22e1aa5a5f 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java @@ -6,6 +6,7 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.camera.core.Camera; import androidx.camera.core.CameraInfo; import androidx.camera.core.CameraSelector; @@ -24,8 +25,8 @@ public class ProcessCameraProviderHostApiImpl implements ProcessCameraProviderHo private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; - private Context context; - private LifecycleOwner lifecycleOwner; + @Nullable private Context context; + @Nullable private LifecycleOwner lifecycleOwner; public ProcessCameraProviderHostApiImpl( @NonNull BinaryMessenger binaryMessenger, @@ -36,7 +37,7 @@ public ProcessCameraProviderHostApiImpl( this.context = context; } - public void setLifecycleOwner(@NonNull LifecycleOwner lifecycleOwner) { + public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) { this.lifecycleOwner = lifecycleOwner; } @@ -57,6 +58,10 @@ public void setContext(@NonNull Context context) { */ @Override public void getInstance(@NonNull GeneratedCameraXLibrary.Result result) { + if (context == null) { + throw new IllegalStateException("Context must be set to get ProcessCameraProvider instance."); + } + ListenableFuture processCameraProviderFuture = ProcessCameraProvider.getInstance(context); @@ -110,6 +115,11 @@ public List getAvailableCameraInfos(@NonNull Long identifier) { @NonNull Long identifier, @NonNull Long cameraSelectorIdentifier, @NonNull List useCaseIds) { + if (lifecycleOwner == null) { + throw new IllegalStateException( + "LifecycleOwner must be set to get ProcessCameraProvider instance."); + } + ProcessCameraProvider processCameraProvider = (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); CameraSelector cameraSelector = diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java index 8f7e8d293f45..74b301e6c0b4 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java @@ -23,7 +23,7 @@ public class RecorderHostApiImpl implements RecorderHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; - private Context context; + @Nullable private Context context; @NonNull @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); @@ -46,6 +46,10 @@ public void create( @Nullable Long aspectRatio, @Nullable Long bitRate, @Nullable Long qualitySelector) { + if (context == null) { + throw new IllegalStateException("Context must be set to create Recorder instance."); + } + Recorder.Builder recorderBuilder = cameraXProxy.createRecorderBuilder(); if (aspectRatio != null) { recorderBuilder.setAspectRatio(aspectRatio.intValue()); @@ -89,6 +93,10 @@ public Long getTargetVideoEncodingBitRate(@NonNull Long identifier) { @NonNull @Override public Long prepareRecording(@NonNull Long identifier, @NonNull String path) { + if (context == null) { + throw new IllegalStateException("Context must be set to prepare recording."); + } + Recorder recorder = getRecorderFromInstanceId(identifier); File temporaryCaptureFile = openTempFile(path); FileOutputOptions fileOutputOptions = diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java index 4e0b18069ffb..d058d62fe224 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java @@ -7,6 +7,7 @@ import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; @@ -19,7 +20,7 @@ public class SystemServicesHostApiImpl implements SystemServicesHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; - private Context context; + @Nullable private Context context; @VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy(); @VisibleForTesting public @NonNull SystemServicesFlutterApiImpl systemServicesFlutterApi; @@ -46,7 +47,7 @@ public void setActivity(@NonNull Activity activity) { this.activity = activity; } - public void setPermissionsRegistry(@NonNull PermissionsRegistry permissionsRegistry) { + public void setPermissionsRegistry(@Nullable PermissionsRegistry permissionsRegistry) { this.permissionsRegistry = permissionsRegistry; } @@ -59,6 +60,10 @@ public void setPermissionsRegistry(@NonNull PermissionsRegistry permissionsRegis @Override public void requestCameraPermissions( @NonNull Boolean enableAudio, @NonNull Result result) { + if (activity == null) { + throw new IllegalStateException("Activity must be set to request camera permissions."); + } + CameraPermissionsManager cameraPermissionsManager = cameraXProxy.createCameraPermissionsManager(); cameraPermissionsManager.requestPermissions( @@ -84,6 +89,10 @@ public void requestCameraPermissions( @Override @NonNull public String getTempFilePath(@NonNull String prefix, @NonNull String suffix) { + if (context == null) { + throw new IllegalStateException("Context must be set to create a temporary file."); + } + try { File path = File.createTempFile(prefix, suffix, context.getCacheDir()); return path.toString(); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java index 58f517edc653..8c9daf8e0143 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java @@ -4,6 +4,8 @@ package io.flutter.plugins.camerax; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; @@ -14,11 +16,14 @@ import android.app.Activity; import android.app.Application; +import android.content.Context; import androidx.lifecycle.LifecycleOwner; import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -30,13 +35,13 @@ public class CameraAndroidCameraxPluginTest { @Mock FlutterPluginBinding flutterPluginBinding; @Test - public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() { - CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); - Activity mockActivity = + public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwnerAsNeeded() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final Activity mockActivity = mock(Activity.class, withSettings().extraInterfaces(LifecycleOwner.class)); - ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = mock(ProcessCameraProviderHostApiImpl.class); - LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); doNothing().when(plugin).setUp(any(), any(), any()); when(activityPluginBinding.getActivity()).thenReturn(mockActivity); @@ -55,12 +60,12 @@ public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() @Test public void - onAttachedToActivity_setsLifecycleOwnerAsProxyLifecycleProviderIfActivityNotLifecycleOwner() { - CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); - Activity mockActivity = mock(Activity.class); - ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + onAttachedToActivity_setsLifecycleOwnerAsProxyLifecycleProviderIfActivityNotLifecycleOwnerAsNeeded() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final Activity mockActivity = mock(Activity.class); + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = mock(ProcessCameraProviderHostApiImpl.class); - LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); doNothing().when(plugin).setUp(any(), any(), any()); when(activityPluginBinding.getActivity()).thenReturn(mockActivity); @@ -78,4 +83,277 @@ public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() .setLifecycleOwner(any(ProxyLifecycleProvider.class)); verify(mockLiveDataHostApiImpl).setLifecycleOwner(any(ProxyLifecycleProvider.class)); } + + @Test + public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final Activity mockActivity = mock(Activity.class); + final SystemServicesHostApiImpl mockSystemServicesHostApiImpl = + mock(SystemServicesHostApiImpl.class); + final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = + mock(DeviceOrientationManagerHostApiImpl.class); + final ArgumentCaptor permissionsRegistryCaptor = + ArgumentCaptor.forClass(PermissionsRegistry.class); + + doNothing().when(plugin).setUp(any(), any(), any()); + when(activityPluginBinding.getActivity()).thenReturn(mockActivity); + when(mockActivity.getApplication()).thenReturn(mock(Application.class)); + + plugin.processCameraProviderHostApiImpl = mock(ProcessCameraProviderHostApiImpl.class); + plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; + plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onAttachedToActivity(activityPluginBinding); + + // Check Activity references are set. + verify(mockSystemServicesHostApiImpl).setActivity(mockActivity); + verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity); + + // Check permissions registry reference is set. + verify(mockSystemServicesHostApiImpl) + .setPermissionsRegistry(permissionsRegistryCaptor.capture()); + assertNotNull(permissionsRegistryCaptor.getValue()); + assertTrue(permissionsRegistryCaptor.getValue() instanceof PermissionsRegistry); + } + + @Test + public void + onDetachedFromActivityForConfigChanges_removesReferencesToActivityPluginBindingAndActivity() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + final SystemServicesHostApiImpl mockSystemServicesHostApiImpl = + mock(SystemServicesHostApiImpl.class); + final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = + mock(DeviceOrientationManagerHostApiImpl.class); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; + plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; + plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onDetachedFromActivityForConfigChanges(); + + verify(mockProcessCameraProviderHostApiImpl).setLifecycleOwner(null); + verify(mockLiveDataHostApiImpl).setLifecycleOwner(null); + verify(mockSystemServicesHostApiImpl).setActivity(null); + verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null); + } + + @Test + public void + onDetachedFromActivityForConfigChanges_setsContextReferencesBasedOnFlutterPluginBinding() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final Context mockContext = mock(Context.class); + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + final RecorderHostApiImpl mockRecorderHostApiImpl = mock(RecorderHostApiImpl.class); + final PendingRecordingHostApiImpl mockPendingRecordingHostApiImpl = + mock(PendingRecordingHostApiImpl.class); + final SystemServicesHostApiImpl mockSystemServicesHostApiImpl = + mock(SystemServicesHostApiImpl.class); + final ImageCaptureHostApiImpl mockImageCaptureHostApiImpl = mock(ImageCaptureHostApiImpl.class); + final ImageAnalysisHostApiImpl mockImageAnalysisHostApiImpl = + mock(ImageAnalysisHostApiImpl.class); + final CameraControlHostApiImpl mockCameraControlHostApiImpl = + mock(CameraControlHostApiImpl.class); + + when(flutterPluginBinding.getApplicationContext()).thenReturn(mockContext); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.recorderHostApiImpl = mockRecorderHostApiImpl; + plugin.pendingRecordingHostApiImpl = mockPendingRecordingHostApiImpl; + plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; + plugin.imageCaptureHostApiImpl = mockImageCaptureHostApiImpl; + plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl; + plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl; + plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onDetachedFromActivityForConfigChanges(); + + verify(mockProcessCameraProviderHostApiImpl).setContext(mockContext); + verify(mockRecorderHostApiImpl).setContext(mockContext); + verify(mockPendingRecordingHostApiImpl).setContext(mockContext); + verify(mockSystemServicesHostApiImpl).setContext(mockContext); + verify(mockImageCaptureHostApiImpl).setContext(mockContext); + verify(mockImageAnalysisHostApiImpl).setContext(mockContext); + verify(mockCameraControlHostApiImpl).setContext(mockContext); + } + + @Test + public void + onReattachedToActivityForConfigChanges_setsLifecycleOwnerAsActivityIfLifecycleOwnerAsNeeded() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final Activity mockActivity = + mock(Activity.class, withSettings().extraInterfaces(LifecycleOwner.class)); + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + + when(activityPluginBinding.getActivity()).thenReturn(mockActivity); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; + plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class); + plugin.deviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); + + plugin.onReattachedToActivityForConfigChanges(activityPluginBinding); + + verify(mockProcessCameraProviderHostApiImpl).setLifecycleOwner(any(LifecycleOwner.class)); + verify(mockLiveDataHostApiImpl).setLifecycleOwner(any(LifecycleOwner.class)); + } + + @Test + public void + onReattachedToActivityForConfigChanges_setsLifecycleOwnerAsProxyLifecycleProviderIfActivityNotLifecycleOwnerAsNeeded() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final Activity mockActivity = mock(Activity.class); + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + + when(activityPluginBinding.getActivity()).thenReturn(mockActivity); + when(mockActivity.getApplication()).thenReturn(mock(Application.class)); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; + plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class); + plugin.deviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onReattachedToActivityForConfigChanges(activityPluginBinding); + + verify(mockProcessCameraProviderHostApiImpl) + .setLifecycleOwner(any(ProxyLifecycleProvider.class)); + verify(mockLiveDataHostApiImpl).setLifecycleOwner(any(ProxyLifecycleProvider.class)); + } + + @Test + public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsRegistryAsNeeded() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final Activity mockActivity = mock(Activity.class); + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + final RecorderHostApiImpl mockRecorderHostApiImpl = mock(RecorderHostApiImpl.class); + final PendingRecordingHostApiImpl mockPendingRecordingHostApiImpl = + mock(PendingRecordingHostApiImpl.class); + final SystemServicesHostApiImpl mockSystemServicesHostApiImpl = + mock(SystemServicesHostApiImpl.class); + final ImageAnalysisHostApiImpl mockImageAnalysisHostApiImpl = + mock(ImageAnalysisHostApiImpl.class); + final ImageCaptureHostApiImpl mockImageCaptureHostApiImpl = mock(ImageCaptureHostApiImpl.class); + final CameraControlHostApiImpl mockCameraControlHostApiImpl = + mock(CameraControlHostApiImpl.class); + final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = + mock(DeviceOrientationManagerHostApiImpl.class); + final ArgumentCaptor permissionsRegistryCaptor = + ArgumentCaptor.forClass(PermissionsRegistry.class); + + when(activityPluginBinding.getActivity()).thenReturn(mockActivity); + when(mockActivity.getApplication()).thenReturn(mock(Application.class)); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.recorderHostApiImpl = mockRecorderHostApiImpl; + plugin.pendingRecordingHostApiImpl = mockPendingRecordingHostApiImpl; + plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; + plugin.imageCaptureHostApiImpl = mockImageCaptureHostApiImpl; + plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl; + plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl; + plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; + plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onReattachedToActivityForConfigChanges(activityPluginBinding); + + // Check Activity references are set. + verify(mockSystemServicesHostApiImpl).setActivity(mockActivity); + verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity); + + // Check Activity as Context references are set. + verify(mockProcessCameraProviderHostApiImpl).setContext(mockActivity); + verify(mockRecorderHostApiImpl).setContext(mockActivity); + verify(mockPendingRecordingHostApiImpl).setContext(mockActivity); + verify(mockSystemServicesHostApiImpl).setContext(mockActivity); + verify(mockImageCaptureHostApiImpl).setContext(mockActivity); + verify(mockImageAnalysisHostApiImpl).setContext(mockActivity); + verify(mockCameraControlHostApiImpl).setContext(mockActivity); + + // Check permissions registry reference is set. + verify(mockSystemServicesHostApiImpl) + .setPermissionsRegistry(permissionsRegistryCaptor.capture()); + assertNotNull(permissionsRegistryCaptor.getValue()); + assertTrue(permissionsRegistryCaptor.getValue() instanceof PermissionsRegistry); + } + + @Test + public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndActivity() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + final SystemServicesHostApiImpl mockSystemServicesHostApiImpl = + mock(SystemServicesHostApiImpl.class); + final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl = + mock(DeviceOrientationManagerHostApiImpl.class); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; + plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; + plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl; + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onDetachedFromActivityForConfigChanges(); + + verify(mockProcessCameraProviderHostApiImpl).setLifecycleOwner(null); + verify(mockLiveDataHostApiImpl).setLifecycleOwner(null); + verify(mockSystemServicesHostApiImpl).setActivity(null); + verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null); + } + + @Test + public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBinding() { + final CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + final Context mockContext = mock(Context.class); + final ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + final RecorderHostApiImpl mockRecorderHostApiImpl = mock(RecorderHostApiImpl.class); + final PendingRecordingHostApiImpl mockPendingRecordingHostApiImpl = + mock(PendingRecordingHostApiImpl.class); + final SystemServicesHostApiImpl mockSystemServicesHostApiImpl = + mock(SystemServicesHostApiImpl.class); + final ImageAnalysisHostApiImpl mockImageAnalysisHostApiImpl = + mock(ImageAnalysisHostApiImpl.class); + final ImageCaptureHostApiImpl mockImageCaptureHostApiImpl = mock(ImageCaptureHostApiImpl.class); + final CameraControlHostApiImpl mockCameraControlHostApiImpl = + mock(CameraControlHostApiImpl.class); + final ArgumentCaptor permissionsRegistryCaptor = + ArgumentCaptor.forClass(PermissionsRegistry.class); + + when(flutterPluginBinding.getApplicationContext()).thenReturn(mockContext); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.recorderHostApiImpl = mockRecorderHostApiImpl; + plugin.pendingRecordingHostApiImpl = mockPendingRecordingHostApiImpl; + plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl; + plugin.imageCaptureHostApiImpl = mockImageCaptureHostApiImpl; + plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl; + plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl; + plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class); + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onDetachedFromActivity(); + + verify(mockProcessCameraProviderHostApiImpl).setContext(mockContext); + verify(mockRecorderHostApiImpl).setContext(mockContext); + verify(mockPendingRecordingHostApiImpl).setContext(mockContext); + verify(mockSystemServicesHostApiImpl).setContext(mockContext); + verify(mockImageCaptureHostApiImpl).setContext(mockContext); + verify(mockImageAnalysisHostApiImpl).setContext(mockContext); + verify(mockCameraControlHostApiImpl).setContext(mockContext); + } } diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index aba0efdbb051..8b8825eed1f2 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.5.0+26 +version: 0.5.0+27 environment: sdk: ">=3.0.0 <4.0.0"