Skip to content

Commit 3ffa737

Browse files
authored
🐛 Predicate access denied to avoid deadlocks (#235)
1 parent b5fcda9 commit 3ffa737

File tree

2 files changed

+89
-53
lines changed

2 files changed

+89
-53
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ See the [Migration Guide](guides/migration_guide.md) for breaking changes betwee
1212

1313
- Use `wechat_picker_library`.
1414

15+
### Fixes
16+
17+
- Predicate access denied to avoid deadlocks.
18+
1519
## 4.2.0-dev.3
1620

1721
### Improvements

lib/src/states/camera_picker_state.dart

Lines changed: 85 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,26 @@ const Duration _kDuration = Duration(milliseconds: 300);
3030

3131
class CameraPickerState extends State<CameraPicker>
3232
with WidgetsBindingObserver {
33+
/// The controller for the current camera.
34+
/// 当前相机实例的控制器
35+
CameraController get controller => innerController!;
36+
CameraController? innerController;
37+
38+
/// Whether the access to the camera or the audio session
39+
/// has been denied by the platform.
40+
bool accessDenied = false;
41+
42+
/// Available cameras.
43+
/// 可用的相机实例
44+
late List<CameraDescription> cameras;
45+
46+
/// Whether the controller is handling method calls.
47+
/// 相机控制器是否在处理方法调用
48+
bool isControllerBusy = false;
49+
50+
/// A [Completer] lock to keep the initialization only runs once at a time.
51+
Completer<void>? initializeLock;
52+
3353
/// The [Duration] for record detection. (200ms)
3454
/// 检测是否开始录制的时长 (200毫秒)
3555
final Duration recordDetectDuration = const Duration(milliseconds: 200);
@@ -47,19 +67,6 @@ class CameraPickerState extends State<CameraPicker>
4767
final ValueNotifier<bool> isFocusPointDisplays = ValueNotifier<bool>(false);
4868
final ValueNotifier<bool> isFocusPointFadeOut = ValueNotifier<bool>(false);
4969

50-
/// The controller for the current camera.
51-
/// 当前相机实例的控制器
52-
CameraController get controller => innerController!;
53-
CameraController? innerController;
54-
55-
/// Available cameras.
56-
/// 可用的相机实例
57-
late List<CameraDescription> cameras;
58-
59-
/// Whether the controller is handling method calls.
60-
/// 相机控制器是否在处理方法调用
61-
bool isControllerBusy = false;
62-
6370
/// Current exposure offset.
6471
/// 当前曝光值
6572
final ValueNotifier<double> currentExposureOffset = ValueNotifier<double>(0);
@@ -253,8 +260,8 @@ class CameraPickerState extends State<CameraPicker>
253260
@override
254261
void didChangeAppLifecycleState(AppLifecycleState state) {
255262
final CameraController? c = innerController;
256-
if (state == AppLifecycleState.resumed) {
257-
initCameras(currentCamera);
263+
if (state == AppLifecycleState.resumed && !accessDenied) {
264+
initCameras(cameraDescription: currentCamera);
258265
} else if (c == null || !c.value.isInitialized) {
259266
// App state changed before we got the chance to initialize.
260267
return;
@@ -319,34 +326,43 @@ class CameraPickerState extends State<CameraPicker>
319326

320327
/// Initialize cameras instances.
321328
/// 初始化相机实例
322-
Future<void> initCameras([CameraDescription? cameraDescription]) async {
323-
// Save the current controller to a local variable.
324-
final CameraController? c = innerController;
325-
// Dispose at last to avoid disposed usage with assertions.
326-
if (c != null) {
327-
innerController = null;
328-
await c.dispose();
329-
}
330-
// Then request a new frame to unbind the controller from elements.
331-
safeSetState(() {
332-
maxAvailableZoom = 1;
333-
minAvailableZoom = 1;
334-
currentZoom = 1;
335-
baseZoom = 1;
336-
// Meanwhile, cancel the existed exposure point and mode display.
337-
exposurePointDisplayTimer?.cancel();
338-
exposureModeDisplayTimer?.cancel();
339-
exposureFadeOutTimer?.cancel();
340-
isFocusPointDisplays.value = false;
341-
isFocusPointFadeOut.value = false;
342-
lastExposurePoint.value = null;
343-
currentExposureOffset.value = 0;
344-
currentExposureSliderOffset.value = 0;
345-
lockedCaptureOrientation = pickerConfig.lockCaptureOrientation;
346-
});
347-
// **IMPORTANT**: Push methods into a post frame callback, which ensures the
348-
// controller has already unbind from widgets.
349-
ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) async {
329+
Future<void> initCameras({
330+
CameraDescription? cameraDescription,
331+
bool ignoreLocks = false,
332+
}) {
333+
if (initializeLock != null && !ignoreLocks) {
334+
return initializeLock!.future;
335+
}
336+
final lock = ignoreLocks ? initializeLock! : Completer<void>();
337+
if (ignoreLocks) {
338+
initializeLock = lock;
339+
}
340+
Future(() async {
341+
// Save the current controller to a local variable.
342+
final CameraController? c = innerController;
343+
// Dispose at last to avoid disposed usage with assertions.
344+
if (c != null) {
345+
innerController = null;
346+
await c.dispose();
347+
}
348+
// Then request a new frame to unbind the controller from elements.
349+
safeSetState(() {
350+
maxAvailableZoom = 1;
351+
minAvailableZoom = 1;
352+
currentZoom = 1;
353+
baseZoom = 1;
354+
// Meanwhile, cancel the existed exposure point and mode display.
355+
exposurePointDisplayTimer?.cancel();
356+
exposureModeDisplayTimer?.cancel();
357+
exposureFadeOutTimer?.cancel();
358+
isFocusPointDisplays.value = false;
359+
isFocusPointFadeOut.value = false;
360+
lastExposurePoint.value = null;
361+
currentExposureOffset.value = 0;
362+
currentExposureSliderOffset.value = 0;
363+
lockedCaptureOrientation = pickerConfig.lockCaptureOrientation;
364+
});
365+
await Future.microtask(() {});
350366
// When the [cameraDescription] is null, which means this is the first
351367
// time initializing cameras, so available cameras should be fetched.
352368
if (cameraDescription == null) {
@@ -388,12 +404,13 @@ class CameraPickerState extends State<CameraPicker>
388404
enableAudio: enableAudio,
389405
imageFormatGroup: pickerConfig.imageFormatGroup,
390406
);
391-
392407
try {
393408
final Stopwatch stopwatch = Stopwatch()..start();
394409
await newController.initialize();
395410
stopwatch.stop();
396-
realDebugPrint("${stopwatch.elapsed} for controller's initialization.");
411+
realDebugPrint(
412+
"${stopwatch.elapsed} for controller's initialization.",
413+
);
397414
// Call recording preparation first.
398415
if (shouldPrepareForVideoRecording) {
399416
stopwatch
@@ -474,18 +491,33 @@ class CameraPickerState extends State<CameraPicker>
474491
stopwatch.stop();
475492
realDebugPrint("${stopwatch.elapsed} for config's update.");
476493
innerController = newController;
494+
lock.complete();
477495
} catch (e, s) {
478-
handleErrorWithHandler(e, s, pickerConfig.onError);
479-
if (!retriedAfterInvalidInitialize) {
480-
retriedAfterInvalidInitialize = true;
481-
Future.delayed(Duration.zero, initCameras);
496+
accessDenied = e is CameraException && e.code.contains('Access');
497+
if (!accessDenied) {
498+
if (!retriedAfterInvalidInitialize) {
499+
retriedAfterInvalidInitialize = true;
500+
Future.delayed(Duration.zero, () {
501+
initCameras(
502+
cameraDescription: cameraDescription,
503+
ignoreLocks: true,
504+
);
505+
});
506+
} else {
507+
retriedAfterInvalidInitialize = false;
508+
lock.completeError(e, s);
509+
}
482510
} else {
483-
retriedAfterInvalidInitialize = false;
511+
lock.completeError(e, s);
484512
}
485-
} finally {
486-
safeSetState(() {});
487513
}
488514
});
515+
return lock.future.catchError((e, s) {
516+
handleErrorWithHandler(e, s, pickerConfig.onError);
517+
}).whenComplete(() {
518+
initializeLock = null;
519+
safeSetState(() {});
520+
});
489521
}
490522

491523
/// Starts to listen on accelerometer events.
@@ -569,7 +601,7 @@ class CameraPickerState extends State<CameraPicker>
569601
if (currentCameraIndex == cameras.length) {
570602
currentCameraIndex = 0;
571603
}
572-
initCameras(currentCamera);
604+
initCameras(cameraDescription: currentCamera);
573605
}
574606

575607
/// Obtain the next camera description for semantics.

0 commit comments

Comments
 (0)