Skip to content

Commit

Permalink
feat: Autopause camera after 1 min of inactivity (#2229)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
g123k and teolemon authored Jun 9, 2022
1 parent c719bff commit 72edce1
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 12 deletions.
13 changes: 11 additions & 2 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1315,5 +1315,14 @@
"add_photo_button_label": "Add photo",
"@add_photo_button_label": {
"description": "Label for the add photo button"
}
}
},
"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"
}
2 changes: 2 additions & 0 deletions packages/smooth_app/lib/pages/scan/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class SmoothCameraController extends CameraController {
}

_isPaused = true;
notifyListeners();
}
}

Expand All @@ -124,6 +125,7 @@ class SmoothCameraController extends CameraController {
await _resumeFlash();
await refocus();
_isPaused = false;
notifyListeners();
}

Future<void> _resumeFlash() async {
Expand Down
67 changes: 65 additions & 2 deletions packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';

import 'package:audioplayers/audioplayers.dart';
import 'package:camera/camera.dart';
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';
Expand Down Expand Up @@ -52,6 +55,9 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
/// 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<int> _averageProcessingTime = AverageList<int>();
final AudioCache _musicPlayer = AudioCache(prefix: 'assets/audio/');
Expand Down Expand Up @@ -84,6 +90,8 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
/// The next time this tab is visible, we will force relaunching the camera.
bool pendingResume = false;

Timer? _inactivityTimeout;

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -145,13 +153,17 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
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<bool>(stoppingCamera),
child: CameraPreview(
_controller!,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaY: blur, sigmaX: blur),
child: CameraPreview(
_controller!,
),
),
),
);
Expand Down Expand Up @@ -290,6 +302,8 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
}

Future<void> _onNewBarcodeDetected(List<String> 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
Expand All @@ -303,8 +317,13 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
);
}
_userPreferences.setFirstScanAchieved();
barcodeAdded = true;
}
}

if (barcodeAdded) {
_startTimerForInactivity();
}
}

void _cameraListener() {
Expand All @@ -319,6 +338,8 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
// TODO(M123): Handle errors better
debugPrint(_controller!.value.errorDescription);
}
} else if (_controller?.isPaused != true) {
_startTimerForInactivity();
}
}

Expand All @@ -331,6 +352,7 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>
_controller!.isPauseResumePreviewSupported != true) {
await _stopImageStream(autoRestart: false);
} else {
_stopTimerForInactivity();
_streamSubscription?.pause();
await _controller?.pausePreview();
}
Expand Down Expand Up @@ -369,6 +391,7 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>

if (_controller?.isPauseResumePreviewSupported == true) {
await _controller?.resumePreviewIfNecessary();
_startTimerForInactivity();
}
stoppingCamera = false;
}
Expand All @@ -380,10 +403,13 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>

stoppingCamera = true;

_stopTimerForInactivity();
if (_controller?.isPauseResumePreviewSupported == true) {
await _controller?.pausePreview();
}

await _controller?.pausePreview();

_redrawScreen();

_controller?.removeListener(_cameraListener);
Expand Down Expand Up @@ -480,6 +506,43 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>

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<bool>(
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';
}
Expand Down
51 changes: 43 additions & 8 deletions packages/smooth_app/lib/pages/scan/scan_visor.dart
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,33 +20,67 @@ class ScannerVisorWidget extends StatefulWidget {
}

class ScannerVisorWidgetState extends State<ScannerVisorWidget> {
@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<GlobalKey<ScannerVisorWidgetState>>(context),
children: <Widget>[
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,
),
),
),
),
Positioned.directional(
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 {
Expand Down

0 comments on commit 72edce1

Please sign in to comment.