blob: acfaf16b9ac46a9301f4e3d2f21e42722b2cdaf7 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:async/async.dart';
import 'package:camera_android_camerax/camera_android_camerax.dart';
import 'package:camera_android_camerax/src/camera.dart';
import 'package:camera_android_camerax/src/camera_info.dart';
import 'package:camera_android_camerax/src/camera_selector.dart';
import 'package:camera_android_camerax/src/camerax_library.g.dart';
import 'package:camera_android_camerax/src/preview.dart';
import 'package:camera_android_camerax/src/process_camera_provider.dart';
import 'package:camera_android_camerax/src/system_services.dart';
import 'package:camera_android_camerax/src/use_case.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/services.dart' show DeviceOrientation;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'android_camera_camerax_test.mocks.dart';
@GenerateNiceMocks(<MockSpec<Object>>[
MockSpec<Camera>(),
MockSpec<CameraInfo>(),
MockSpec<CameraSelector>(),
MockSpec<Preview>(),
MockSpec<ProcessCameraProvider>(),
])
@GenerateMocks(<Type>[BuildContext])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('Should fetch CameraDescription instances for available cameras',
() async {
// Arrange
final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax();
camera.processCameraProvider = MockProcessCameraProvider();
final List<dynamic> returnData = <dynamic>[
<String, dynamic>{
'name': 'Camera 0',
'lensFacing': 'back',
'sensorOrientation': 0
},
<String, dynamic>{
'name': 'Camera 1',
'lensFacing': 'front',
'sensorOrientation': 90
}
];
// Create mocks to use
final MockCameraInfo mockFrontCameraInfo = MockCameraInfo();
final MockCameraInfo mockBackCameraInfo = MockCameraInfo();
// Mock calls to native platform
when(camera.processCameraProvider!.getAvailableCameraInfos()).thenAnswer(
(_) async => <MockCameraInfo>[mockBackCameraInfo, mockFrontCameraInfo]);
when(camera.mockBackCameraSelector
.filter(<MockCameraInfo>[mockFrontCameraInfo]))
.thenAnswer((_) async => <MockCameraInfo>[]);
when(camera.mockBackCameraSelector
.filter(<MockCameraInfo>[mockBackCameraInfo]))
.thenAnswer((_) async => <MockCameraInfo>[mockBackCameraInfo]);
when(camera.mockFrontCameraSelector
.filter(<MockCameraInfo>[mockBackCameraInfo]))
.thenAnswer((_) async => <MockCameraInfo>[]);
when(camera.mockFrontCameraSelector
.filter(<MockCameraInfo>[mockFrontCameraInfo]))
.thenAnswer((_) async => <MockCameraInfo>[mockFrontCameraInfo]);
when(mockBackCameraInfo.getSensorRotationDegrees())
.thenAnswer((_) async => 0);
when(mockFrontCameraInfo.getSensorRotationDegrees())
.thenAnswer((_) async => 90);
final List<CameraDescription> cameraDescriptions =
await camera.availableCameras();
expect(cameraDescriptions.length, returnData.length);
for (int i = 0; i < returnData.length; i++) {
final Map<String, Object?> typedData =
(returnData[i] as Map<dynamic, dynamic>).cast<String, Object?>();
final CameraDescription cameraDescription = CameraDescription(
name: typedData['name']! as String,
lensDirection: (typedData['lensFacing']! as String) == 'front'
? CameraLensDirection.front
: CameraLensDirection.back,
sensorOrientation: typedData['sensorOrientation']! as int,
);
expect(cameraDescriptions[i], cameraDescription);
}
});
test(
'createCamera requests permissions, starts listening for device orientation changes, and returns flutter surface texture ID',
() async {
final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax();
camera.processCameraProvider = MockProcessCameraProvider();
const CameraLensDirection testLensDirection = CameraLensDirection.back;
const int testSensorOrientation = 90;
const CameraDescription testCameraDescription = CameraDescription(
name: 'cameraName',
lensDirection: testLensDirection,
sensorOrientation: testSensorOrientation);
const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh;
const bool enableAudio = true;
const int testSurfaceTextureId = 6;
when(camera.testPreview.setSurfaceProvider())
.thenAnswer((_) async => testSurfaceTextureId);
expect(
await camera.createCamera(testCameraDescription, testResolutionPreset,
enableAudio: enableAudio),
equals(testSurfaceTextureId));
// Verify permissions are requested and the camera starts listening for device orientation changes.
expect(camera.cameraPermissionsRequested, isTrue);
expect(camera.startedListeningForDeviceOrientationChanges, isTrue);
// Verify CameraSelector is set with appropriate lens direction.
expect(camera.cameraSelector, equals(camera.mockBackCameraSelector));
// Verify the camera's Preview instance is instantiated properly.
expect(camera.preview, equals(camera.testPreview));
// Verify the camera's Preview instance has its surface provider set.
verify(camera.preview!.setSurfaceProvider());
});
test(
'initializeCamera throws AssertionError when createCamera has not been called before initializedCamera',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
expect(() => camera.initializeCamera(3), throwsAssertionError);
});
test('initializeCamera sends expected CameraInitializedEvent', () async {
final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax();
camera.processCameraProvider = MockProcessCameraProvider();
const int cameraId = 10;
const CameraLensDirection testLensDirection = CameraLensDirection.back;
const int testSensorOrientation = 90;
const CameraDescription testCameraDescription = CameraDescription(
name: 'cameraName',
lensDirection: testLensDirection,
sensorOrientation: testSensorOrientation);
const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh;
const bool enableAudio = true;
const int resolutionWidth = 350;
const int resolutionHeight = 750;
final Camera mockCamera = MockCamera();
final ResolutionInfo testResolutionInfo =
ResolutionInfo(width: resolutionWidth, height: resolutionHeight);
// TODO(camsim99): Modify this when camera configuration is supported and
// defualt values no longer being used.
// https://github.com/flutter/flutter/issues/120468
// https://github.com/flutter/flutter/issues/120467
final CameraInitializedEvent testCameraInitializedEvent =
CameraInitializedEvent(
cameraId,
resolutionWidth.toDouble(),
resolutionHeight.toDouble(),
ExposureMode.auto,
false,
FocusMode.auto,
false);
// Call createCamera.
when(camera.testPreview.setSurfaceProvider())
.thenAnswer((_) async => cameraId);
await camera.createCamera(testCameraDescription, testResolutionPreset,
enableAudio: enableAudio);
when(camera.processCameraProvider!.bindToLifecycle(
camera.cameraSelector!, <UseCase>[camera.testPreview]))
.thenAnswer((_) async => mockCamera);
when(camera.testPreview.getResolutionInfo())
.thenAnswer((_) async => testResolutionInfo);
// Start listening to camera events stream to verify the proper CameraInitializedEvent is sent.
camera.cameraEventStreamController.stream.listen((CameraEvent event) {
expect(event, const TypeMatcher<CameraInitializedEvent>());
expect(event, equals(testCameraInitializedEvent));
});
await camera.initializeCamera(cameraId);
// Verify preview was bound and unbound to get preview resolution information.
verify(camera.processCameraProvider!.bindToLifecycle(
camera.cameraSelector!, <UseCase>[camera.testPreview]));
verify(camera.processCameraProvider!.unbind(<UseCase>[camera.testPreview]));
// Check camera instance was received, but preview is no longer bound.
expect(camera.camera, equals(mockCamera));
expect(camera.previewIsBound, isFalse);
});
test('dispose releases Flutter surface texture and unbinds all use cases',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
camera.preview = MockPreview();
camera.processCameraProvider = MockProcessCameraProvider();
camera.dispose(3);
verify(camera.preview!.releaseFlutterSurfaceTexture());
verify(camera.processCameraProvider!.unbindAll());
});
test('onCameraInitialized stream emits CameraInitializedEvents', () async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
const int cameraId = 16;
final Stream<CameraInitializedEvent> eventStream =
camera.onCameraInitialized(cameraId);
final StreamQueue<CameraInitializedEvent> streamQueue =
StreamQueue<CameraInitializedEvent>(eventStream);
const CameraInitializedEvent testEvent = CameraInitializedEvent(
cameraId, 320, 80, ExposureMode.auto, false, FocusMode.auto, false);
camera.cameraEventStreamController.add(testEvent);
expect(await streamQueue.next, testEvent);
await streamQueue.cancel();
});
test('onCameraError stream emits errors caught by system services', () async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
const int cameraId = 27;
const String testErrorDescription = 'Test error description!';
final Stream<CameraErrorEvent> eventStream = camera.onCameraError(cameraId);
final StreamQueue<CameraErrorEvent> streamQueue =
StreamQueue<CameraErrorEvent>(eventStream);
SystemServices.cameraErrorStreamController.add(testErrorDescription);
expect(await streamQueue.next,
equals(const CameraErrorEvent(cameraId, testErrorDescription)));
await streamQueue.cancel();
});
test(
'onDeviceOrientationChanged stream emits changes in device oreintation detected by system services',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
final Stream<DeviceOrientationChangedEvent> eventStream =
camera.onDeviceOrientationChanged();
final StreamQueue<DeviceOrientationChangedEvent> streamQueue =
StreamQueue<DeviceOrientationChangedEvent>(eventStream);
const DeviceOrientationChangedEvent testEvent =
DeviceOrientationChangedEvent(DeviceOrientation.portraitDown);
SystemServices.deviceOrientationChangedStreamController.add(testEvent);
expect(await streamQueue.next, testEvent);
await streamQueue.cancel();
});
test(
'pausePreview unbinds preview from lifecycle when preview is nonnull and has been bound to lifecycle',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
camera.processCameraProvider = MockProcessCameraProvider();
camera.preview = MockPreview();
camera.previewIsBound = true;
await camera.pausePreview(579);
verify(camera.processCameraProvider!.unbind(<UseCase>[camera.preview!]));
expect(camera.previewIsBound, isFalse);
});
test(
'pausePreview does not unbind preview from lifecycle when preview has not been bound to lifecycle',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
camera.processCameraProvider = MockProcessCameraProvider();
camera.preview = MockPreview();
await camera.pausePreview(632);
verifyNever(
camera.processCameraProvider!.unbind(<UseCase>[camera.preview!]));
});
test('resumePreview does not bind preview to lifecycle if already bound',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
camera.processCameraProvider = MockProcessCameraProvider();
camera.cameraSelector = MockCameraSelector();
camera.preview = MockPreview();
camera.previewIsBound = true;
await camera.resumePreview(78);
verifyNever(camera.processCameraProvider!
.bindToLifecycle(camera.cameraSelector!, <UseCase>[camera.preview!]));
});
test('resumePreview binds preview to lifecycle if not already bound',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
camera.processCameraProvider = MockProcessCameraProvider();
camera.cameraSelector = MockCameraSelector();
camera.preview = MockPreview();
await camera.resumePreview(78);
verify(camera.processCameraProvider!
.bindToLifecycle(camera.cameraSelector!, <UseCase>[camera.preview!]));
});
test(
'buildPreview returns a FutureBuilder that does not return a Texture until the preview is bound to the lifecycle',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
const int textureId = 75;
camera.processCameraProvider = MockProcessCameraProvider();
camera.cameraSelector = MockCameraSelector();
camera.preview = MockPreview();
final FutureBuilder<void> previewWidget =
camera.buildPreview(textureId) as FutureBuilder<void>;
expect(
previewWidget.builder(
MockBuildContext(), const AsyncSnapshot<void>.nothing()),
isA<SizedBox>());
expect(
previewWidget.builder(
MockBuildContext(), const AsyncSnapshot<void>.waiting()),
isA<SizedBox>());
expect(
previewWidget.builder(MockBuildContext(),
const AsyncSnapshot<void>.withData(ConnectionState.active, null)),
isA<SizedBox>());
});
test(
'buildPreview returns a FutureBuilder that returns a Texture once the preview is bound to the lifecycle',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
const int textureId = 75;
camera.processCameraProvider = MockProcessCameraProvider();
camera.cameraSelector = MockCameraSelector();
camera.preview = MockPreview();
final FutureBuilder<void> previewWidget =
camera.buildPreview(textureId) as FutureBuilder<void>;
final Texture previewTexture = previewWidget.builder(MockBuildContext(),
const AsyncSnapshot<void>.withData(ConnectionState.done, null))
as Texture;
expect(previewTexture.textureId, equals(textureId));
});
}
/// Mock of [AndroidCameraCameraX] that stubs behavior of some methods for
/// testing.
class MockAndroidCameraCamerax extends AndroidCameraCameraX {
bool cameraPermissionsRequested = false;
bool startedListeningForDeviceOrientationChanges = false;
final MockPreview testPreview = MockPreview();
final MockCameraSelector mockBackCameraSelector = MockCameraSelector();
final MockCameraSelector mockFrontCameraSelector = MockCameraSelector();
@override
Future<void> requestCameraPermissions(bool enableAudio) async {
cameraPermissionsRequested = true;
}
@override
void startListeningForDeviceOrientationChange(
bool cameraIsFrontFacing, int sensorOrientation) {
startedListeningForDeviceOrientationChanges = true;
return;
}
@override
CameraSelector createCameraSelector(int cameraSelectorLensDirection) {
switch (cameraSelectorLensDirection) {
case CameraSelector.lensFacingFront:
return mockFrontCameraSelector;
case CameraSelector.lensFacingBack:
default:
return mockBackCameraSelector;
}
}
@override
Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) {
return testPreview;
}
}