diff --git a/app/lib/app_init.dart b/app/lib/app_init.dart index 7786c35d..eed97956 100644 --- a/app/lib/app_init.dart +++ b/app/lib/app_init.dart @@ -40,6 +40,7 @@ import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/android/android_info.dart'; import 'package:nc_photos/mobile/self_signed_cert_manager.dart'; import 'package:nc_photos/platform/features.dart' as features; +import 'package:nc_photos/session_storage.dart'; import 'package:nc_photos/touch_manager.dart'; import 'package:np_db/np_db.dart'; import 'package:np_gps_map/np_gps_map.dart'; @@ -73,6 +74,8 @@ Future init(InitIsolateType isolateType) async { await _initDiContainer(isolateType); _initVisibilityDetector(); GpsMap.init(); + // init session storage + SessionStorage(); _hasInitedInThisIsolate = true; } diff --git a/app/lib/protected_page_handler.dart b/app/lib/protected_page_handler.dart index 80017515..edaf162a 100644 --- a/app/lib/protected_page_handler.dart +++ b/app/lib/protected_page_handler.dart @@ -34,7 +34,7 @@ extension ProtectedPageBuildContextExtension on NavigatorState { U? result, Object? arguments, }) async { - if (await _auth()) { + if (await authProtectedPage()) { return pushReplacementNamed(routeName, arguments: arguments, result: result); } else { @@ -46,7 +46,7 @@ extension ProtectedPageBuildContextExtension on NavigatorState { String routeName, { Object? arguments, }) async { - if (await _auth()) { + if (await authProtectedPage()) { return pushNamed(routeName, arguments: arguments); } else { throw const ProtectedPageAuthException(); @@ -54,14 +54,14 @@ extension ProtectedPageBuildContextExtension on NavigatorState { } Future pushProtected(Route route) async { - if (await _auth()) { + if (await authProtectedPage()) { return push(route); } else { throw const ProtectedPageAuthException(); } } - Future _auth() async { + Future authProtectedPage() async { final securePrefController = context.read(); switch (securePrefController.protectedPageAuthTypeValue) { case null: diff --git a/app/lib/session_storage.dart b/app/lib/session_storage.dart index fc122920..e93c08b8 100644 --- a/app/lib/session_storage.dart +++ b/app/lib/session_storage.dart @@ -1,3 +1,5 @@ +import 'package:clock/clock.dart'; + /// Hold non-persisted global variables class SessionStorage { factory SessionStorage() { @@ -15,5 +17,7 @@ class SessionStorage { /// Whether the dynamic_color library is supported in this platform bool isSupportDynamicColor = false; + DateTime lastSuspendTime = clock.now(); + static final _inst = SessionStorage._(); } diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index e370b47f..b352acfd 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:clock/clock.dart'; import 'package:copy_with/copy_with.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/gestures.dart'; @@ -15,6 +18,7 @@ import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/legacy/connect.dart' as legacy; import 'package:nc_photos/legacy/sign_in.dart' as legacy; import 'package:nc_photos/navigation_manager.dart'; +import 'package:nc_photos/protected_page_handler.dart'; import 'package:nc_photos/session_storage.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; @@ -54,6 +58,7 @@ import 'package:np_db/np_db.dart'; import 'package:to_string/to_string.dart'; part 'my_app.g.dart'; +part 'my_app/app_lock.dart'; part 'my_app/bloc.dart'; part 'my_app/state_event.dart'; @@ -171,9 +176,9 @@ class _WrappedAppState extends State<_WrappedApp> @override void dispose() { - super.dispose(); SnackBarManager().unregisterHandler(this); NavigationManager().unsetHandler(this); + super.dispose(); } @override @@ -560,7 +565,7 @@ class _ThemedMyApp extends StatelessWidget { systemNavigationBarColor: theme.colorScheme.secondaryContainer, systemNavigationBarIconBrightness: theme.brightness.invert(), ), - child: child, + child: _AppLockMyApp(child: child), ); } diff --git a/app/lib/widget/my_app.g.dart b/app/lib/widget/my_app.g.dart index 2d0c17c3..f16449ee 100644 --- a/app/lib/widget/my_app.g.dart +++ b/app/lib/widget/my_app.g.dart @@ -66,6 +66,20 @@ extension _$_WrappedAppStateNpLog on _WrappedAppState { static final log = Logger("widget.my_app._WrappedAppState"); } +extension _$_AppLockMyAppStateNpLog on _AppLockMyAppState { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.my_app._AppLockMyAppState"); +} + +extension _$_AppLockOverlayPageStateNpLog on _AppLockOverlayPageState { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.my_app._AppLockOverlayPageState"); +} + extension _$_BlocNpLog on _Bloc { // ignore: unused_element Logger get _log => log; diff --git a/app/lib/widget/my_app/app_lock.dart b/app/lib/widget/my_app/app_lock.dart new file mode 100644 index 00000000..f1d1854a --- /dev/null +++ b/app/lib/widget/my_app/app_lock.dart @@ -0,0 +1,141 @@ +part of '../my_app.dart'; + +class _AppLockMyApp extends StatefulWidget { + const _AppLockMyApp({ + required this.child, + }); + + @override + State createState() => _AppLockMyAppState(); + + final Widget child; +} + +@npLog +class _AppLockMyAppState extends State<_AppLockMyApp> { + @override + void initState() { + super.initState(); + _lifecycleListener = AppLifecycleListener( + onHide: () { + SessionStorage().lastSuspendTime = clock.now(); + }, + onShow: () async { + final now = clock.now(); + final diff = now.difference(SessionStorage().lastSuspendTime); + _log.info("Suspended for: $diff"); + if (diff >= const Duration(seconds: 30) && !_shouldLock) { + _log.info("Suspended for too long, auth required"); + setState(() { + _shouldLock = true; + }); + late final OverlayEntry authOverlay; + authOverlay = OverlayEntry( + builder: (_) => _AppLockOverlay( + onAuthSuccess: () { + authOverlay.remove(); + if (mounted) { + setState(() { + _shouldLock = false; + }); + } + }, + ), + ); + _key.currentState?.insert(authOverlay); + } + }, + ); + } + + @override + void dispose() { + _lifecycleListener.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Overlay( + key: _key, + initialEntries: [ + OverlayEntry( + maintainState: true, + builder: (_) => widget.child, + ), + ], + ); + } + + late final AppLifecycleListener _lifecycleListener; + final _key = GlobalKey(); + var _shouldLock = false; +} + +class _AppLockOverlay extends StatelessWidget { + const _AppLockOverlay({ + required this.onAuthSuccess, + }); + + @override + Widget build(BuildContext context) { + return HeroControllerScope.none( + child: Navigator( + onGenerateRoute: (_) => MaterialPageRoute( + builder: (context) => + _AppLockOverlayPage(onAuthSuccess: onAuthSuccess), + ), + ), + ); + } + + final VoidCallback onAuthSuccess; +} + +class _AppLockOverlayPage extends StatefulWidget { + const _AppLockOverlayPage({ + required this.onAuthSuccess, + }); + + @override + State createState() => _AppLockOverlayPageState(); + + final VoidCallback onAuthSuccess; +} + +@npLog +class _AppLockOverlayPageState extends State<_AppLockOverlayPage> { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _auth(); + } + }); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: Container( + color: Colors.black, + child: const Align( + alignment: Alignment(0, .75), + child: Icon(Icons.lock_outlined, size: 64), + ), + ), + ); + } + + Future _auth() async { + if (mounted && await Navigator.of(context).authProtectedPage()) { + widget.onAuthSuccess(); + } else { + _log.warning("[_auth] Auth failed"); + await Future.delayed(const Duration(seconds: 2)); + unawaited(_auth()); + } + } +}