From 46092b5fcf3e5a43d96af5f16339554f5b15c8a1 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Tue, 11 Jul 2023 15:41:16 +0200 Subject: [PATCH 1/2] On iOS, the camera was never stopped after being resumed in some edge cases --- .../ml_kit/lib/src/scanner_ml_kit.dart | 70 +++++++++++++++---- packages/scanner/ml_kit/pubspec.yaml | 2 + packages/smooth_app/pubspec.lock | 2 +- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart b/packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart index c34384ace10..32ae056693f 100644 --- a/packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart +++ b/packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:scanner_shared/scanner_shared.dart'; @@ -100,6 +101,11 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit> autoStart: true, ); + // Stores a background operation when the screen isn't visible + CancelableOperation? _autoStopCameraOperation; + // Stores the latest visibility value of the screen + VisibilityInfo? _latestVisibilityInfoEvent; + @override void initState() { super.initState(); @@ -113,25 +119,58 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit> if (state == AppLifecycleState.paused) { _stop(); } else if (state == AppLifecycleState.resumed) { - /// When the app is resumed (from the launcher for example), the camera is - /// always started and we can't prevent this behavior. - /// - /// To fix it, we check when the app is resumed if the camera is the - /// visible page and if that's not the case, we wait for the camera to be - /// initialized to stop it - WidgetsBinding.instance.addPostFrameCallback((_) { - if (ScreenVisibilityDetector.invisible(context)) { - _pauseCameraWhenInitialized(); - } - }); + _autoStopCameraOperation?.cancel(); + _checkIfAppIsRestarting(); + } + } + + void _checkIfAppIsRestarting([int retry = 0]) { + /// When the app is resumed (from the launcher for example), the camera is + /// always started and we can't prevent this behavior. + /// + /// To fix it, we check when the app is resumed if the camera is the + /// visible page and if that's not the case, we wait for the camera to be + /// initialized to stop it + // ignore: prefer_function_declarations_over_variables + final Function fn = () { + if (ScreenVisibilityDetector.invisible(context)) { + _pauseCameraWhenInitialized(); + } else if (retry < 1) { + // In 99% of cases, this won't happen, but if for some reason, we are + // "considered" as visible, we will retry in a few milliseconds + // and if we are still invisible -> force stop the camera + _autoStopCameraOperation = CancelableOperation.fromFuture( + Future.delayed( + const Duration(milliseconds: 500), + () => _checkIfAppIsRestarting(retry + 1), + ), + ); + } else if (_latestVisibilityInfoEvent?.visible == false) { + _pauseCameraWhenInitialized(); + } + }; + + // Ensure to wait for the first frame + if (retry == 0) { + // ignore: avoid_dynamic_calls + WidgetsBinding.instance.addPostFrameCallback((_) => fn.call()); + } else { + // ignore: avoid_dynamic_calls + scheduleMicrotask(() => fn.call()); } } Future _pauseCameraWhenInitialized() async { + if (!mounted) { + return; + } + if (_controller.isStarting) { - return Future.delayed( - const Duration(milliseconds: 250), - () => _pauseCameraWhenInitialized(), + _autoStopCameraOperation = CancelableOperation.fromFuture( + Future.delayed( + const Duration(milliseconds: 250), + () => _pauseCameraWhenInitialized(), + ), ); } @@ -155,6 +194,7 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit> } Future _stop() async { + _autoStopCameraOperation?.cancel(); if (!_isStarted) { return; } @@ -172,6 +212,7 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit> return VisibilityDetector( key: _visibilityKey, onVisibilityChanged: (final VisibilityInfo info) async { + _latestVisibilityInfoEvent = info; if (info.visibleBounds.height > 0.0) { await _start(); } else { @@ -300,6 +341,7 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit> @override void dispose() { WidgetsBinding.instance.removeObserver(this); + _autoStopCameraOperation?.cancel(); _controller.dispose(); super.dispose(); } diff --git a/packages/scanner/ml_kit/pubspec.yaml b/packages/scanner/ml_kit/pubspec.yaml index 9acba17987f..2a1f8e93a1b 100644 --- a/packages/scanner/ml_kit/pubspec.yaml +++ b/packages/scanner/ml_kit/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: sdk: flutter visibility_detector: 0.4.0+2 + async: 2.11.0 + mobile_scanner: git: url: https://github.com/openfoodfacts/mobile_scanner.git diff --git a/packages/smooth_app/pubspec.lock b/packages/smooth_app/pubspec.lock index 8e6bc3fcdef..53735b41c36 100644 --- a/packages/smooth_app/pubspec.lock +++ b/packages/smooth_app/pubspec.lock @@ -78,7 +78,7 @@ packages: source: hosted version: "7.0.0" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" From 43d38405ecf11d8686a8d106965f36424dcb93f6 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Fri, 14 Jul 2023 23:58:09 +0200 Subject: [PATCH 2/2] Add a comment to link to the PR --- packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart b/packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart index 32ae056693f..0b0fc97b13a 100644 --- a/packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart +++ b/packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart @@ -126,11 +126,17 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit> void _checkIfAppIsRestarting([int retry = 0]) { /// When the app is resumed (from the launcher for example), the camera is - /// always started and we can't prevent this behavior. + /// always started due to the [autostart] feature and we can't + /// prevent this behavior. /// /// To fix it, we check when the app is resumed if the camera is the /// visible page and if that's not the case, we wait for the camera to be - /// initialized to stop it + /// initialized to stop it. + /// + /// Comment from @g123k: This is a very hacky way (temporary I hope) and + /// more explanation are available on the PR: + /// [https://github.com/openfoodfacts/smooth-app/pull/4292] + /// // ignore: prefer_function_declarations_over_variables final Function fn = () { if (ScreenVisibilityDetector.invisible(context)) {