Skip to content

[camerax] Implement onCameraClosing #3878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 91 commits into from
May 9, 2023
Merged

Conversation

camsim99
Copy link
Contributor

@camsim99 camsim99 commented May 1, 2023

Overview

Continuation of #3419.

Implements onCameraClosing that will send an event anytime the camera closes, as detected by observing the LiveData of the CameraState of the current camera being used in the plugin.

Fixes flutter/flutter#121162.

Highlights of what has changed since the latest reviews of the last PR:

  • Wrapped LiveData#getValue (docs) for the purposes of retrieving current value LiveData<ZoomState> to properly implement retrieving zoom information
  • Modified Dart Host API and FlutterAPI implementations' documentation on their binaryMessenger and instanceManager documentation to be clearer as per suggestion from comments on the original PR
  • Changed binaryMessenger and instanceManager fields to be private in Flutter API implementations
  • Removed cast method and added enum LiveDataSupportedType to pigeon file to use for safely casting the LiveData generic when received on the Dart or native side.
  • Moved CameraStateError code decoding to Dart side as this was written by me and not directly provided by CameraX.
  • Added tests for the conversion of objects.
  • The following issues were filed to capture feedback given but not addressed in this PR: [camerax] ☂️ Clean up wrapped native classes flutter#125926, [camerax] ☂️ Clean up Dart Tests flutter#125928.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the package surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.
  • I updated CHANGELOG.md to add a description of the change, following repository CHANGELOG style.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

@@ -312,17 +332,33 @@ class AndroidCameraCameraX extends CameraPlatform {
/// [cameraId] not used.
@override
Future<double> getMaxZoomLevel(int cameraId) async {
final ZoomState exposureState = await cameraInfo!.getZoomState();
return exposureState.maxZoomRatio;
final LiveData<ZoomState> liveZoomState = await cameraInfo!.getZoomState();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not be better to have a local zoomState variable that is updated by an Observer added to liveZoomState? Same for getMinZoomLevel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, because I think it's safe to assume this won't be called often enough for us to want to set an Observer on it. If there was a Flutter camera method that suggested we'd want to observe changes in the zoom state, I would agree with you.

/// camera is in error state.
Observer<CameraState> _createCameraClosingObserver(int cameraId) {
// Callback method used to implement the behavior described above:
void onChanged(Object stateAsObject) {
Copy link
Contributor

@bparrishMines bparrishMines May 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Garbage collection for Dart is non-deterministic, so testing for it is a little difficult. I do have a test for leaking in the webview_flutter integration tests that verifies the WebView is garbage collected: https://github.com/flutter/packages/blob/main/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart#L100

I don't think the implementation of the test should block this PR though. Leaks only prevent the release of the strong reference to the object in the Java InstanceManager. And the camera resources are explicitly released with the methods provided by the API.

I can also help with writing the test.


if (zoomState == null) {
throw CameraException(
'zoomStateNotSet',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a constant. Both because it is used in multiple places and because callers that want to branch behavior on different CameraExceptions should be able to reference constants instead of strings.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved but not a constant and no comment explaining why.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're looking at outdated code?

@@ -447,11 +486,11 @@ class AndroidCameraCameraX extends CameraPlatform {
height: imageProxy.height,
width: imageProxy.width);

cameraImageDataStreamController?.add(cameraImageData);
weakThis.target!.cameraImageDataStreamController?.add(cameraImageData);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the ? is ok for cameraImageDataStreamController why is it not ok for target?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made them both ! because neither should be null!

Future<T?>? getValueFromInstances<T extends Object>(
LiveData<T> instance) async {
LiveDataSupportedTypeData? typeData;
switch (T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!


/// Adds specified [Observer] instance to instance manager and makes call
/// to native side to create the instance.
Future<void> createFromInstances<T>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be createFromInstance (singular)?

Copy link
Contributor

@reidbaker reidbaker May 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was marked resolved but I dont see anything explaining why it should be plural. Non blocking but I am still curious for the answer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're also looking at outdated code here; I changed it to singular.


expect(callbackParameter, value);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test to make sure createFromInstances copies the values correctly?

Same for other classes if a similar method exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled in the host API create tests. createFromInstances calls through to the mockApis so when we test that call, we ensure createFromInstances is handling the values correctly

Copy link
Contributor

@bparrishMines bparrishMines left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

///
/// [shouldCreateDetachedObjectForTesting] is for testing only and thus,
/// is set to `false` by default.
AndroidCameraCameraX({this.shouldCreateDetachedObjectForTesting = false});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this implementation is it possible to call AndroidCameraCameraX(shouldCreateDetachedObjectForTesting: true) from non testing code?

My gut says yes it is and that instead this should be something like

Suggested change
AndroidCameraCameraX({this.shouldCreateDetachedObjectForTesting = false});
AndroidCameraCameraX():this._shouldCreateDetachedObjectForTesting = false;
@visibleForTesting
AndroidCameraCameraX.forTesting(this._shouldCreateDetachedObjectForTesting );

Notice how the parameter is still final but now private and the value is protected by the constructor being visible for testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's still possible to change from non-testing code :/

How would I change this from testing code, though, with this structure? I need a way to set this to true for testing, so is there a way to do that without exposing it also to non-testing code? I would think using a setter, but that's still visible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a patch file with it working you can apply.
protected-testing-object.patch

@@ -404,7 +450,10 @@ class AndroidCameraCameraX extends CameraPlatform {

/// Binds [preview] instance to the camera lifecycle controlled by the
/// [processCameraProvider].
Future<void> _bindPreviewToLifecycle() async {
///
/// [cameraId] used to properly update the live camera state for the camera
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non blocking: This still does not help me as someone who does not know the implementation what I should do as a caller.

For example [cameraId] the camera reference id that will have its [CameraState] updated when the process for preview is rebound (if required)

I don't actually know if that is true but that is sort of what I see when I read the code. It implies if you pass a bad camera id you might not get state updates but that everything else will probably work.

@@ -447,11 +499,14 @@ class AndroidCameraCameraX extends CameraPlatform {
height: imageProxy.height,
width: imageProxy.width);

cameraImageDataStreamController?.add(cameraImageData);
weakThis.target!.cameraImageDataStreamController!.add(cameraImageData);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is likely that I just misunderstand how our weakThis works but wouldnt the point of a weak reference be that target can be null?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second issue, best I can tell this is in _bindPreviewToLifecycle is there a promised relationship that this method is called after onStreamedFrameAvailable? if not then cameraImageDataStreamController I think can be null.

Reading this line of code I see 2 places where it should be expected that we cant set cameraImageData.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First issue: @bparrishMines am I misunderstanding how weakThis should be used? Should I allow for the target to be null to avoid leaks?

Second issue: Yes, this is a helper method used to set up the image streaming that is called from listening to the stream returned by onStreamedFrameAvailable, so it should never be called really any other time. cameraImageDataStreamController is set up before that, so it would never be null here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the point of the weak reference is to ensure the callback method doesn't prevent garbage collection of the instance that contains it. The target COULD be null, but the ! is just a runtime check that can be used if you expect the target to always be available in the callback.

For example, if you expect the user to eventually ALWAYS call CameraController.dispose and you do some kind of cleanup in that method that stops image streaming callbacks. Then I think it is reasonable to expect this to always be nonnull. But a test would also be helpful for ensuring this and preventing a null pointer exception.

As a comparison, weview_flutter always uses weakThis.target?... because there is no WebViewController.dispose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you described is what we can expect because dispose must be called whenever users want to release camera resources. I didn't properly handle this for image streaming so I handled that and updated the test for dispose.

/// If a previous [liveCameraState] was stored, existing observers are
/// removed, as well.
Future<void> _updateLiveCameraState(int cameraId) async {
final CameraInfo cameraInfo = await camera!.getCameraInfo();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non blocking. I still find this weird that we need to pass a cameraId (and use that name) when we have a reference to a camera object already and we dont use one to validate against the other. Is there an oppetunity to use more descriptive variable names here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this gets at the bigger question as to why cameraId is used throughout the Flutter camera API to begin with. Really, cameraId is the ID of the Flutter surface texture used to render a preview to, and thus, is almost totally unrelated to the actual camera instance we use to do any camera functionality. The Flutter surface texture ID is returned by calls to createCamera(...), so I assume it is just an ID that folks using the plugin can mostly use to filter through CameraEvents added to the various camera event streams (because they are constructed with an ID) because otherwise, at least in the Android camera implementations, it goes mostly unused.

I'm on the fence about renaming cameraId, because while it would be more useful to name it something more like flutterSurfaceTextureId, it's not named that in the actual camera platform interface, so it could cause more confusion for folks trying to contribute to the plugin.

However, I am on board for bettering documentation in the implementation around what it is (always referring to the Flutter surface texture ID/createCamera), when it is actually being used in a method, and if it is being used, exactly how. I'll file an issue before landing this for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flutter/flutter#126270 filed for improving documentation

void onChanged(Object stateAsObject) {
final WeakReference<AndroidCameraCameraX> weakThis =
WeakReference<AndroidCameraCameraX>(this);
final CameraState state = stateAsObject as CameraState;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment to that effect since one class is relying on the known implementation of another class and not on its api surface.

@camsim99 camsim99 added the autosubmit Merge PR when tree becomes green via auto submit App label May 9, 2023
@auto-submit auto-submit bot merged commit c62f6f7 into flutter:main May 9, 2023
nploi pushed a commit to nploi/packages that referenced this pull request Jul 16, 2023
### Overview

Continuation of flutter#3419.

Implements onCameraClosing that will send an event anytime the camera closes, as detected by observing the LiveData of the CameraState of the current camera being used in the plugin.

Fixes flutter/flutter#121162.

### Highlights of what has changed since the latest reviews of the [last PR](flutter#3419):

- Wrapped `LiveData#getValue` ([docs](https://developer.android.com/reference/androidx/lifecycle/LiveData#getValue())) for the purposes of retrieving current value `LiveData<ZoomState>` to properly implement retrieving zoom information
- Modified Dart Host API and FlutterAPI implementations' documentation on their `binaryMessenger` and `instanceManager` documentation to be clearer as per suggestion from comments on the original PR
- Changed `binaryMessenger` and `instanceManager` fields to be private in Flutter API implementations
- Removed `cast` method and added enum `LiveDataSupportedType` to pigeon file to use for safely casting the `LiveData` generic when received on the Dart or native side.
- Moved `CameraStateError` code decoding to Dart side as this was written by me and not directly provided by CameraX.
- Added tests for the conversion of objects.
- The following issues were filed to capture feedback given but not addressed in this PR: flutter/flutter#125926, flutter/flutter#125928.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: camera platform-android
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[camerax] Implement onCameraClosing
3 participants