Skip to content

Commit

Permalink
feat: On iOS, the camera was never stopped after being resumed in som…
Browse files Browse the repository at this point in the history
…e edge cases (#4292)
  • Loading branch information
g123k authored Jul 15, 2023
1 parent 75dd24e commit 00f42ae
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 15 deletions.
76 changes: 62 additions & 14 deletions packages/scanner/ml_kit/lib/src/scanner_ml_kit.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -100,6 +101,11 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit>
autoStart: true,
);

// Stores a background operation when the screen isn't visible
CancelableOperation<void>? _autoStopCameraOperation;
// Stores the latest visibility value of the screen
VisibilityInfo? _latestVisibilityInfoEvent;

@override
void initState() {
super.initState();
Expand All @@ -113,25 +119,64 @@ 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 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.
///
/// 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)) {
_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<void>.fromFuture(
Future<void>.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<void> _pauseCameraWhenInitialized() async {
if (!mounted) {
return;
}

if (_controller.isStarting) {
return Future<void>.delayed(
const Duration(milliseconds: 250),
() => _pauseCameraWhenInitialized(),
_autoStopCameraOperation = CancelableOperation<void>.fromFuture(
Future<void>.delayed(
const Duration(milliseconds: 250),
() => _pauseCameraWhenInitialized(),
),
);
}

Expand All @@ -155,6 +200,7 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit>
}

Future<void> _stop() async {
_autoStopCameraOperation?.cancel();
if (!_isStarted) {
return;
}
Expand All @@ -172,6 +218,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 {
Expand Down Expand Up @@ -300,6 +347,7 @@ class _SmoothBarcodeScannerMLKitState extends State<_SmoothBarcodeScannerMLKit>
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_autoStopCameraOperation?.cancel();
_controller.dispose();
super.dispose();
}
Expand Down
2 changes: 2 additions & 0 deletions packages/scanner/ml_kit/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/smooth_app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ packages:
source: hosted
version: "7.0.0"
async:
dependency: transitive
dependency: "direct main"
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
Expand Down

0 comments on commit 00f42ae

Please sign in to comment.