Skip to content
Draft
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
88 changes: 88 additions & 0 deletions packages/camera/camera_android_camerax/AGENTS.md

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This might not be the final AGENTS.md we use in the plugin, but included my draft for now in case some of the core information was useful for creating the implementation plan.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#12066 is my actual proposal.

Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Agent Guide for camera_android_camerax

This document provides context, architectural details, and core workflows for making high quality contributions to the `camera_android_camerax` package.

## Architectural Overview

The `camera_android_camerax` package is the Android platform implementation of the `camera` plugin, using the **Android Jetpack CameraX** library.

### ProxyApi Binding System
This plugin utilizes **Pigeon's ProxyApi** to bind Dart objects directly to native Android CameraX Java/Kotlin objects.
- **Pigeon Definition**: The API surfaces are defined in [pigeons/camerax_library.dart](pigeons/camerax_library.dart).
- **Generated Code**:
- Dart proxy classes: [lib/src/camerax_library.g.dart](lib/src/camerax_library.g.dart)
- Kotlin proxy classes: [android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt](android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt)
- **Plugin Entry Point**: [lib/src/android_camera_camerax.dart](lib/src/android_camera_camerax.dart) implements `CameraPlatform` using the generated proxy classes.

## Required Steps Before Making Any Changes

Before making any changes, run the the [check-readiness skill](.agents/skills/check-readiness/SKILL.md) to ensure your environment has all the required tooling.

## Required Steps After Making Any Changes

1. **Regenerate Code (if applicable)**:
- If you modified [pigeons/camerax_library.dart](pigeons/camerax_library.dart), run the Pigeon generation command under [Code Generation](#code-generation).
- If you modified any classes that are mocked or added new mocks, run the Mockito generation command under [Code Generation](#code-generation).
2. **Verify Tests**:
- Ensure you have added or updated unit tests to cover your changes.
- Run and pass all tests (see [Running Tests](#running-tests) for details):
- Dart unit tests.
- Android native unit tests.
- Integration tests.
3. **Static Analysis**:
- Do not rely on ad-hoc or generic commands to check for formatting or lint errors. Always use the [dart-run-static-analysis skill](.agents/skills/dart-run-static-analysis/SKILL.md) during development to analyze the code and apply automated fixes.

## Code Generation

When you modify the Pigeon API [pigeons/camerax_library.dart](pigeons/camerax_library.dart), you must regenerate the respective code to update the Dart proxy class [lib/src/camerax_library.g.dart](lib/src/camerax_library.g.dart) and the Kotlin proxy class [android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt](android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt). Run from this directory:

```bash
dart run pigeon --input pigeons/camerax_library.dart
```

Because test in this package use `mockito` for mocking, you must also regenerate the Mockito mocks that are used for unit testing any changes you make. So, also run from this directory:

```bash
dart run build_runner build -d
```

## Running Tests

When you make a change, add a test if you can (either a Dart unit test, Android native unit test, or Flutter integration tests). Regardless of if tests are added or not, all tests must pass after you make changes. How to run the tests:

### Dart Unit Tests
Dart unit tests are located in [test/](./test/).
To run them from this directory:
```bash
dart run ../../../script/tool/bin/flutter_plugin_tools.dart dart-test --packages=camera_android_camerax
```

### Android Native Unit Tests
Android unit tests are located in [android/src/test/](./android/src/test/) and run using Robolectric.
To run them from this directory:
```bash
dart run ../../../script/tool/bin/flutter_plugin_tools.dart native-test --android --packages=camera_android_camerax
```

### Flutter Integration Tests
Integration tests are located in [example/integration_test/integration_test.dart](./example/integration_test/integration_test.dart).
With an emulator or physical device connected, run from this directory:
```bash
dart run ../../../script/tool/bin/flutter_plugin_tools.dart integration-test --android --packages=camera_android_camerax
```

## Required Steps Before Pushing

You MUST read and follow the [pre-push skill](.agents/skills/pre-push-skill/SKILL.md) immediately whenever:
- The user asks to push changes.
- The user asks if you or they are ready to push.
- The user wants to validate that local changes are ready to become a pull request.
- It is the final step when you are ready to make a PR and consider an issue solved.

This skill will execute all the required pre-push checks (e.g., tests, publish checks, license formatting) for the `flutter/packages` repository.

## Agent Coding and Review Guidelines

- **Communication and Code Review**: When receiving code review feedback, DO NOT be overly accommodating or blindly agree with the user if the feedback seems technically questionable. Instead, use the [receiving-code-review skill](.agents/skills/receiving-code-review/SKILL.md) to apply technical rigor and verify the suggestions. Be direct if you believe the feedback is incorrect.
- **Code Quality and Complexity**: Do not produce low-quality or overly complex code. For complex features, propose using the `/grill-me` or `/plan` slash commands to create a design plan before writing code. Enforce strict minimum test coverage and cognitive complexity standards. Use the [dart-add-unit-test skill](.agents/skills/dart-add-unit-test/SKILL.md) and [dart-collect-coverage skill](.agents/skills/dart-collect-coverage/SKILL.md) to ensure high coverage.
- **Duplicate Code**: Avoid duplicating code, especially constant strings. Instead of duplicating, look for existing patterns in adjacent code and extract shared values into constants.
137 changes: 137 additions & 0 deletions packages/camera/camera_android_camerax/implementation_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
## Fix NullPointerException on backgrounding during active video recording
The `NullPointerException` occurs because the Flutter-side state representing an active recording becomes desynced from the Native CameraX state when the app goes into the background.

When an app using the `camera` plugin is backgrounded, the `CameraController` automatically calls `dispose()` to tear down the camera resources. This invokes `processCameraProvider?.unbindAll()` in the `camera_android_camerax` plugin.

**Native Cleanup Clarity:**
When `processCameraProvider?.unbindAll()` is called natively, CameraX unbinds the `VideoCapture` use case. This action inherently stops any active recording on the native side. CameraX gracefully finalizes the recording and saves the video file to the disk without corrupting it. No hanging `Recording` instances are left behind natively.

However, the `dispose` method on the Dart side does not clear the `recording` and `pendingRecording` objects. When the app is resumed, the singleton `AndroidCameraCameraX` still thinks the previous recording is active (`recording != null`). When the user tries to start a new recording, `startVideoCapturing` returns silently. When they click the "Stop" button, `stopVideoRecording` attempts to stop the old recording by calling `await recording!.close()`. Since the Native CameraX `Recorder` was already finalized, this throws a `java.lang.NullPointerException`.
Comment thread
camsim99 marked this conversation as resolved.

## User Review Required
No major architectural shifts or breaking changes are introduced. This is a straightforward bug fix to clean up internal state during teardown.

## Open Questions
None. The root cause and fix are well understood.

---

## Proposed Changes

### camera_android_camerax package
We will update `dispose()` to clean up the video recording state so it correctly aligns with native behavior upon teardown.

**Why `dispose()`?**
`dispose()` is called when the camera needs to be fully torn down—either because the app is backgrounding or the user is switching to a different camera (e.g., front to back). Since `AndroidCameraCameraX` operates as a singleton, failing to clear the recording state during `dispose()` means the next camera initialized will erroneously think a recording is still active. Clearing the state in `dispose()` guarantees a clean slate for the next camera session, regardless of the trigger (backgrounding, camera switching, or hot restart).

#### [MODIFY] android_camera_camerax.dart
Add state cleanup for `recording`, `pendingRecording`, and `videoOutputPath` in the `dispose` method.

```diff
/// Releases the resources of the accessed camera with ID [cameraId].
@override
Future<void> dispose(int cameraId) async {
Comment thread
camsim99 marked this conversation as resolved.
await preview?.releaseSurfaceProvider();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(this change can be considered out of scope) Do these need to be awaited in order? can we instead record a future for each of them then await the set?

await liveCameraState?.removeObservers();
await processCameraProvider?.unbindAll();
await imageAnalysis?.clearAnalyzer();
await deviceOrientationManager.stopListeningForDeviceOrientationChange();
+ recording = null;
+ pendingRecording = null;
+ videoOutputPath = null;
Comment thread
camsim99 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something in the code above stores the video safely when the camera is disposed. Instead of setting these values to null here can we make that code null out the recording?

For example if there is a lifecycle event from the camerax library for native recording stop can we forward that call instead of relying on the dispose? (maybe dispose is the call being forwarded?)

}
```

#### [MODIFY] android_camera_camerax_test.dart
Add assertions in the `dispose` test to ensure that the recording state variables are properly nullified when `dispose()` completes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FWIW It feels like a pattern that information about camera is in a bunch of variables that need their state cleared. Is there a better pattern for this that we can use. Maybe a CameraState object that we can call dispose on that handles all its own variables and makes the lifecycle more clear.


```diff
test(
'dispose releases Flutter surface texture, removes camera state observers, and unbinds all use cases',
() async {
+ // Setup mock recording state
+ camera.recording = MockRecording();
+ camera.pendingRecording = MockPendingRecording();
+ camera.videoOutputPath = 'test/path.mp4';
+
await camera.dispose(3);

verify(mockPreview.releaseSurfaceProvider());
verify(mockLiveCameraState.removeObservers());
verify(mockProcessCameraProvider.unbindAll());
verify(mockImageAnalysis.clearAnalyzer());
verify(mockDeviceOrientationManager
.stopListeningForDeviceOrientationChange());
+
+ // Verify state is cleared

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can this test also ensure that the video was saved correctly?

+ expect(camera.recording, isNull);
+ expect(camera.pendingRecording, isNull);
+ expect(camera.videoOutputPath, isNull);
});
```

#### [MODIFY] example/integration_test/integration_test.dart
Add a Flutter integration test mimicking the app lifecycle pause/resume while recording. (Flutter integration tests run on devices using Espresso underneath).

```dart
testWidgets(
'video recording state is cleared after camera is disposed',
(WidgetTester tester) async {
final CameraController cameraController = CameraController(
cameras[0],
ResolutionPreset.low,
);
await cameraController.initialize();
await cameraController.startVideoRecording();

// Dispose the controller, which simulates what the example app does
// when the AppLifecycleState becomes inactive (e.g. backgrounding).
await cameraController.dispose();

// Create a new controller (simulating app resume)
final CameraController newController = CameraController(
cameras[0],
ResolutionPreset.low,
);
await newController.initialize();

// Attempt to start a new recording. This should not throw or silently fail.
await newController.startVideoRecording();

// Stop it, ensuring no NPE is thrown by the native side.
final XFile file = await newController.stopVideoRecording();
expect(file, isNotNull);

await newController.dispose();
});
```
Comment thread
camsim99 marked this conversation as resolved.

## Verification Plan

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In our first plan, we had it explicitly list out the steps it would take to make sure that the code is high quality. I'm ok with that not being explicitly included because it will be in our AGENTS.md and the skills, but just noting in case other reviewers do not agree.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

for now I think the verification steps should be listed especially if there are no artifacts of that verification.

For a human, this would be something like, I ran it on a pixel device and swapped between front facing and rear facing camera and it the example app did not crash.

Verification ensures the app gracefully handles backgrounding during recordings and cleanly starts new recordings upon resume.

### Automated Tests (What I can do autonomously)
As an AI agent, I am unable to launch a local Android Emulator or connect to a physical Android device, meaning I cannot run integration tests (`integration-test`) or manual UI verification myself. However, I can compile the application to ensure it builds correctly, and I can run all local unit tests.

I will run the following commands:
```bash
# Verify the example APK builds successfully without errors
cd example/android && ./gradlew assembleDebug

Comment on lines +115 to +119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dont this is the way we want to verify the android code builds. It should use the flutter tool.

# Run unit tests to verify the Dart logic works as expected
dart run ../../../script/tool/bin/flutter_plugin_tools.dart dart-test --packages=camera_android_camerax
```

### Manual Verification (What requires human interaction/devices)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

See comment thread about this being reviewer verification.

Because I do not have access to an emulator or physical device, a human developer is needed to run the integration tests and verify the UI on a device.

1. Run the integration test on an attached device:
```bash
dart run ../../../script/tool/bin/flutter_plugin_tools.dart integration-test --android --packages=camera_android_camerax
```
2. Run the example app (`example/lib/main.dart`) on an Android device.
3. Click the video camera icon to start recording a new video.
4. Background the app (e.g., navigate to the home screen).
5. Resume the app.
6. Click the video camera icon to start recording a new video.
7. Click the stop icon to stop recording.
8. Verify the app does not crash, the recording preview is displayed, and the second video is successfully saved to the device.
Loading