From 3ffa73768d8fe36568a22ea6fc5f71e5d15e5e17 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Mon, 26 Feb 2024 20:27:41 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Predicate=20access=20denied=20to?= =?UTF-8?q?=20avoid=20deadlocks=20(#235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 + lib/src/states/camera_picker_state.dart | 138 +++++++++++++++--------- 2 files changed, 89 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 675e96e..827f87d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ See the [Migration Guide](guides/migration_guide.md) for breaking changes betwee - Use `wechat_picker_library`. +### Fixes + +- Predicate access denied to avoid deadlocks. + ## 4.2.0-dev.3 ### Improvements diff --git a/lib/src/states/camera_picker_state.dart b/lib/src/states/camera_picker_state.dart index e62dd9c..fb9bc66 100644 --- a/lib/src/states/camera_picker_state.dart +++ b/lib/src/states/camera_picker_state.dart @@ -30,6 +30,26 @@ const Duration _kDuration = Duration(milliseconds: 300); class CameraPickerState extends State with WidgetsBindingObserver { + /// The controller for the current camera. + /// 当前相机实例的控制器 + CameraController get controller => innerController!; + CameraController? innerController; + + /// Whether the access to the camera or the audio session + /// has been denied by the platform. + bool accessDenied = false; + + /// Available cameras. + /// 可用的相机实例 + late List cameras; + + /// Whether the controller is handling method calls. + /// 相机控制器是否在处理方法调用 + bool isControllerBusy = false; + + /// A [Completer] lock to keep the initialization only runs once at a time. + Completer? initializeLock; + /// The [Duration] for record detection. (200ms) /// 检测是否开始录制的时长 (200毫秒) final Duration recordDetectDuration = const Duration(milliseconds: 200); @@ -47,19 +67,6 @@ class CameraPickerState extends State final ValueNotifier isFocusPointDisplays = ValueNotifier(false); final ValueNotifier isFocusPointFadeOut = ValueNotifier(false); - /// The controller for the current camera. - /// 当前相机实例的控制器 - CameraController get controller => innerController!; - CameraController? innerController; - - /// Available cameras. - /// 可用的相机实例 - late List cameras; - - /// Whether the controller is handling method calls. - /// 相机控制器是否在处理方法调用 - bool isControllerBusy = false; - /// Current exposure offset. /// 当前曝光值 final ValueNotifier currentExposureOffset = ValueNotifier(0); @@ -253,8 +260,8 @@ class CameraPickerState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? c = innerController; - if (state == AppLifecycleState.resumed) { - initCameras(currentCamera); + if (state == AppLifecycleState.resumed && !accessDenied) { + initCameras(cameraDescription: currentCamera); } else if (c == null || !c.value.isInitialized) { // App state changed before we got the chance to initialize. return; @@ -319,34 +326,43 @@ class CameraPickerState extends State /// Initialize cameras instances. /// 初始化相机实例 - Future initCameras([CameraDescription? cameraDescription]) async { - // Save the current controller to a local variable. - final CameraController? c = innerController; - // Dispose at last to avoid disposed usage with assertions. - if (c != null) { - innerController = null; - await c.dispose(); - } - // Then request a new frame to unbind the controller from elements. - safeSetState(() { - maxAvailableZoom = 1; - minAvailableZoom = 1; - currentZoom = 1; - baseZoom = 1; - // Meanwhile, cancel the existed exposure point and mode display. - exposurePointDisplayTimer?.cancel(); - exposureModeDisplayTimer?.cancel(); - exposureFadeOutTimer?.cancel(); - isFocusPointDisplays.value = false; - isFocusPointFadeOut.value = false; - lastExposurePoint.value = null; - currentExposureOffset.value = 0; - currentExposureSliderOffset.value = 0; - lockedCaptureOrientation = pickerConfig.lockCaptureOrientation; - }); - // **IMPORTANT**: Push methods into a post frame callback, which ensures the - // controller has already unbind from widgets. - ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) async { + Future initCameras({ + CameraDescription? cameraDescription, + bool ignoreLocks = false, + }) { + if (initializeLock != null && !ignoreLocks) { + return initializeLock!.future; + } + final lock = ignoreLocks ? initializeLock! : Completer(); + if (ignoreLocks) { + initializeLock = lock; + } + Future(() async { + // Save the current controller to a local variable. + final CameraController? c = innerController; + // Dispose at last to avoid disposed usage with assertions. + if (c != null) { + innerController = null; + await c.dispose(); + } + // Then request a new frame to unbind the controller from elements. + safeSetState(() { + maxAvailableZoom = 1; + minAvailableZoom = 1; + currentZoom = 1; + baseZoom = 1; + // Meanwhile, cancel the existed exposure point and mode display. + exposurePointDisplayTimer?.cancel(); + exposureModeDisplayTimer?.cancel(); + exposureFadeOutTimer?.cancel(); + isFocusPointDisplays.value = false; + isFocusPointFadeOut.value = false; + lastExposurePoint.value = null; + currentExposureOffset.value = 0; + currentExposureSliderOffset.value = 0; + lockedCaptureOrientation = pickerConfig.lockCaptureOrientation; + }); + await Future.microtask(() {}); // When the [cameraDescription] is null, which means this is the first // time initializing cameras, so available cameras should be fetched. if (cameraDescription == null) { @@ -388,12 +404,13 @@ class CameraPickerState extends State enableAudio: enableAudio, imageFormatGroup: pickerConfig.imageFormatGroup, ); - try { final Stopwatch stopwatch = Stopwatch()..start(); await newController.initialize(); stopwatch.stop(); - realDebugPrint("${stopwatch.elapsed} for controller's initialization."); + realDebugPrint( + "${stopwatch.elapsed} for controller's initialization.", + ); // Call recording preparation first. if (shouldPrepareForVideoRecording) { stopwatch @@ -474,18 +491,33 @@ class CameraPickerState extends State stopwatch.stop(); realDebugPrint("${stopwatch.elapsed} for config's update."); innerController = newController; + lock.complete(); } catch (e, s) { - handleErrorWithHandler(e, s, pickerConfig.onError); - if (!retriedAfterInvalidInitialize) { - retriedAfterInvalidInitialize = true; - Future.delayed(Duration.zero, initCameras); + accessDenied = e is CameraException && e.code.contains('Access'); + if (!accessDenied) { + if (!retriedAfterInvalidInitialize) { + retriedAfterInvalidInitialize = true; + Future.delayed(Duration.zero, () { + initCameras( + cameraDescription: cameraDescription, + ignoreLocks: true, + ); + }); + } else { + retriedAfterInvalidInitialize = false; + lock.completeError(e, s); + } } else { - retriedAfterInvalidInitialize = false; + lock.completeError(e, s); } - } finally { - safeSetState(() {}); } }); + return lock.future.catchError((e, s) { + handleErrorWithHandler(e, s, pickerConfig.onError); + }).whenComplete(() { + initializeLock = null; + safeSetState(() {}); + }); } /// Starts to listen on accelerometer events. @@ -569,7 +601,7 @@ class CameraPickerState extends State if (currentCameraIndex == cameras.length) { currentCameraIndex = 0; } - initCameras(currentCamera); + initCameras(cameraDescription: currentCamera); } /// Obtain the next camera description for semantics.