From 72edce13d622f20d3a24f52d6f8bf5719e93ff87 Mon Sep 17 00:00:00 2001 From: Edouard Marquez Date: Thu, 9 Jun 2022 11:23:17 +0200 Subject: [PATCH] feat: Autopause camera after 1 min of inactivity (#2229) * Autopause camera after 1 min of inactivity * Restart the camera timeout after a successful scan * Change camera timeout dialog content Co-authored-by: Pierre Slamich --- packages/smooth_app/lib/l10n/app_en.arb | 13 +++- .../lib/pages/scan/camera_controller.dart | 2 + .../lib/pages/scan/ml_kit_scan_page.dart | 67 ++++++++++++++++++- .../smooth_app/lib/pages/scan/scan_visor.dart | 51 +++++++++++--- 4 files changed, 121 insertions(+), 12 deletions(-) diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 0090344ebda..e2bfc17e11f 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1315,5 +1315,14 @@ "add_photo_button_label": "Add photo", "@add_photo_button_label": { "description": "Label for the add photo button" - } -} \ No newline at end of file + }, + "camera_paused_dialog_title": "Camera paused", + "camera_paused_dialog_content": "To save energy, the scanner has been automatically turned off after {min} min of inactivity", + "@camera_paused_dialog_content": { + "placeholders": { + "min": {} + } + }, + "camera_paused_dialog_positive_label": "Restart camera", + "camera_paused_dialog_negative_label": "Stay paused" +} diff --git a/packages/smooth_app/lib/pages/scan/camera_controller.dart b/packages/smooth_app/lib/pages/scan/camera_controller.dart index dafdecd4d35..e971dddc401 100644 --- a/packages/smooth_app/lib/pages/scan/camera_controller.dart +++ b/packages/smooth_app/lib/pages/scan/camera_controller.dart @@ -103,6 +103,7 @@ class SmoothCameraController extends CameraController { } _isPaused = true; + notifyListeners(); } } @@ -124,6 +125,7 @@ class SmoothCameraController extends CameraController { await _resumeFlash(); await refocus(); _isPaused = false; + notifyListeners(); } Future _resumeFlash() async { diff --git a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart index f2328562c8b..56bef10f20c 100644 --- a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math' as math; +import 'dart:ui'; import 'package:audioplayers/audioplayers.dart'; import 'package:camera/camera.dart'; @@ -7,11 +8,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; import 'package:smooth_app/data_models/continuous_scan_model.dart'; import 'package:smooth_app/data_models/user_preferences.dart'; +import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/helpers/camera_helper.dart'; import 'package:smooth_app/helpers/collections_helper.dart'; import 'package:smooth_app/pages/page_manager.dart'; @@ -52,6 +55,9 @@ class MLKitScannerPageState extends LifecycleAwareState /// Minimal processing windows between two decodings static const int _processingTimeWindows = 5; + /// Period after which the camera will be paused + static const Duration _inactivityPeriod = Duration(minutes: 1); + /// A time window is the average time decodings took final AverageList _averageProcessingTime = AverageList(); final AudioCache _musicPlayer = AudioCache(prefix: 'assets/audio/'); @@ -84,6 +90,8 @@ class MLKitScannerPageState extends LifecycleAwareState /// The next time this tab is visible, we will force relaunching the camera. bool pendingResume = false; + Timer? _inactivityTimeout; + @override void initState() { super.initState(); @@ -145,13 +153,17 @@ class MLKitScannerPageState extends LifecycleAwareState return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { _computePreviewScale(constraints); + final double blur = (_controller?.isPaused == true) ? 5.0 : 0.0; return Transform.scale( scale: _previewScale, child: Center( key: ValueKey(stoppingCamera), - child: CameraPreview( - _controller!, + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaY: blur, sigmaX: blur), + child: CameraPreview( + _controller!, + ), ), ), ); @@ -290,6 +302,8 @@ class MLKitScannerPageState extends LifecycleAwareState } Future _onNewBarcodeDetected(List barcodes) async { + bool barcodeAdded = false; + for (final String barcode in barcodes) { if (await _model.onScan(barcode)) { // Both are Future methods, but it doesn't matter to wait here @@ -303,8 +317,13 @@ class MLKitScannerPageState extends LifecycleAwareState ); } _userPreferences.setFirstScanAchieved(); + barcodeAdded = true; } } + + if (barcodeAdded) { + _startTimerForInactivity(); + } } void _cameraListener() { @@ -319,6 +338,8 @@ class MLKitScannerPageState extends LifecycleAwareState // TODO(M123): Handle errors better debugPrint(_controller!.value.errorDescription); } + } else if (_controller?.isPaused != true) { + _startTimerForInactivity(); } } @@ -331,6 +352,7 @@ class MLKitScannerPageState extends LifecycleAwareState _controller!.isPauseResumePreviewSupported != true) { await _stopImageStream(autoRestart: false); } else { + _stopTimerForInactivity(); _streamSubscription?.pause(); await _controller?.pausePreview(); } @@ -369,6 +391,7 @@ class MLKitScannerPageState extends LifecycleAwareState if (_controller?.isPauseResumePreviewSupported == true) { await _controller?.resumePreviewIfNecessary(); + _startTimerForInactivity(); } stoppingCamera = false; } @@ -380,10 +403,13 @@ class MLKitScannerPageState extends LifecycleAwareState stoppingCamera = true; + _stopTimerForInactivity(); if (_controller?.isPauseResumePreviewSupported == true) { await _controller?.pausePreview(); } + await _controller?.pausePreview(); + _redrawScreen(); _controller?.removeListener(_cameraListener); @@ -480,6 +506,43 @@ class MLKitScannerPageState extends LifecycleAwareState SmoothCameraController? get _controller => CameraHelper.controller; + /// Starts (or restarts) the timer for inactivity + void _startTimerForInactivity() { + _stopTimerForInactivity(); + + _inactivityTimeout = Timer(_inactivityPeriod, () async { + await CameraHelper.controller?.pausePreview(); + _stopTimerForInactivity(); + + showDialog( + context: context, + builder: (BuildContext context) { + final AppLocalizations localizations = AppLocalizations.of(context); + + return SmoothAlertDialog( + title: localizations.camera_paused_dialog_title, + body: Text(localizations + .camera_paused_dialog_content(_inactivityPeriod.inMinutes)), + positiveAction: SmoothActionButton( + text: localizations.camera_paused_dialog_positive_label, + onPressed: () async { + Navigator.of(context).pop(); + CameraHelper.controller?.resumePreviewIfNecessary(); + }), + negativeAction: SmoothActionButton( + text: localizations.camera_paused_dialog_negative_label, + onPressed: () { + Navigator.of(context).pop(); + }), + ); + }); + }); + } + + void _stopTimerForInactivity() { + _inactivityTimeout?.cancel(); + } + @override String get traceTitle => 'ml_kit_scan_page'; } diff --git a/packages/smooth_app/lib/pages/scan/scan_visor.dart b/packages/smooth_app/lib/pages/scan/scan_visor.dart index 2d302636cd7..51f784019f3 100644 --- a/packages/smooth_app/lib/pages/scan/scan_visor.dart +++ b/packages/smooth_app/lib/pages/scan/scan_visor.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; +import 'package:smooth_app/helpers/camera_helper.dart'; import 'package:smooth_app/pages/scan/scan_flash_toggle.dart'; /// This Widget is a [StatefulWidget], as it uses a [GlobalKey] to allow an @@ -19,21 +20,42 @@ class ScannerVisorWidget extends StatefulWidget { } class ScannerVisorWidgetState extends State { + @override + void initState() { + super.initState(); + + if (mounted) { + CameraHelper.controller?.addListener(onCameraChanged); + } + } + @override Widget build(BuildContext context) { + final bool isPaused = CameraHelper.controller?.isPaused ?? false; + return Stack( key: Provider.of>(context), children: [ - SizedBox.fromSize( - size: ScannerVisorWidget.getSize(context), + GestureDetector( + onTap: isPaused + ? () { + CameraHelper.controller?.resumePreviewIfNecessary(); + } + : null, child: CustomPaint( painter: ScanVisorPainter(), child: Center( - child: SvgPicture.asset( - 'assets/icons/visor_icon.svg', - width: 35.0, - height: 32.0, - ), + child: isPaused + ? const Icon( + Icons.pause_circle_outline, + color: Colors.white, + size: 40.0, + ) + : SvgPicture.asset( + 'assets/icons/visor_icon.svg', + width: 35.0, + height: 32.0, + ), ), ), ), @@ -41,11 +63,24 @@ class ScannerVisorWidgetState extends State { textDirection: Directionality.of(context), end: 0.0, bottom: 0.0, - child: const ScannerFlashToggleWidget(), + child: Offstage( + offstage: isPaused, + child: const ScannerFlashToggleWidget(), + ), ) ], ); } + + void onCameraChanged() { + setState(() {}); + } + + @override + void dispose() { + CameraHelper.controller?.removeListener(onCameraChanged); + super.dispose(); + } } class ScanVisorPainter extends CustomPainter {