ente/lib/ui/tools/app_lock.dart
Manav 3170cf48c0 Pass the localizations to the Material App created in AppLock
Will be needed in the future when we start accessing the context in showDialog
children. For more details, see the sibling auth PR
https://github.com/ente-io/auth/pull/29.
2023-01-08 16:33:13 +05:30

196 lines
6 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// A widget which handles app lifecycle events for showing and hiding a lock screen.
/// This should wrap around a `MyApp` widget (or equivalent).
///
/// [lockScreen] is a [Widget] which should be a screen for handling login logic and
/// calling `AppLock.of(context).didUnlock();` upon a successful login.
///
/// [builder] is a [Function] taking an [Object] as its argument and should return a
/// [Widget]. The [Object] argument is provided by the [lockScreen] calling
/// `AppLock.of(context).didUnlock();` with an argument. [Object] can then be injected
/// in to your `MyApp` widget (or equivalent).
///
/// [enabled] determines wether or not the [lockScreen] should be shown on app launch
/// and subsequent app pauses. This can be changed later on using `AppLock.of(context).enable();`,
/// `AppLock.of(context).disable();` or the convenience method `AppLock.of(context).setEnabled(enabled);`
/// using a bool argument.
///
/// [backgroundLockLatency] determines how much time is allowed to pass when
/// the app is in the background state before the [lockScreen] widget should be
/// shown upon returning. It defaults to instantly.
///
// ignore_for_file: unnecessary_this, library_private_types_in_public_api
class AppLock extends StatefulWidget {
final Widget Function(Object?) builder;
final Widget lockScreen;
final bool enabled;
final Duration backgroundLockLatency;
final ThemeData? darkTheme;
final ThemeData? lightTheme;
const AppLock({
Key? key,
required this.builder,
required this.lockScreen,
this.enabled = true,
this.backgroundLockLatency = const Duration(seconds: 0),
this.darkTheme,
this.lightTheme,
}) : super(key: key);
static _AppLockState? of(BuildContext context) =>
context.findAncestorStateOfType<_AppLockState>();
@override
State<AppLock> createState() => _AppLockState();
}
class _AppLockState extends State<AppLock> with WidgetsBindingObserver {
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
late bool _didUnlockForAppLaunch;
late bool _isLocked;
late bool _enabled;
Timer? _backgroundLockLatencyTimer;
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
this._didUnlockForAppLaunch = !this.widget.enabled;
this._isLocked = false;
this._enabled = this.widget.enabled;
super.initState();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!this._enabled) {
return;
}
if (state == AppLifecycleState.paused &&
(!this._isLocked && this._didUnlockForAppLaunch)) {
this._backgroundLockLatencyTimer =
Timer(this.widget.backgroundLockLatency, () => this.showLockScreen());
}
if (state == AppLifecycleState.resumed) {
this._backgroundLockLatencyTimer?.cancel();
}
super.didChangeAppLifecycleState(state);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
this._backgroundLockLatencyTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: this.widget.enabled ? this._lockScreen : this.widget.builder(null),
navigatorKey: _navigatorKey,
themeMode: ThemeMode.system,
theme: widget.lightTheme,
darkTheme: widget.darkTheme,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
onGenerateRoute: (settings) {
switch (settings.name) {
case '/lock-screen':
return PageRouteBuilder(
pageBuilder: (_, __, ___) => this._lockScreen,
);
case '/unlocked':
return PageRouteBuilder(
pageBuilder: (_, __, ___) =>
this.widget.builder(settings.arguments),
);
}
return PageRouteBuilder(pageBuilder: (_, __, ___) => this._lockScreen);
},
);
}
Widget get _lockScreen {
return WillPopScope(
child: this.widget.lockScreen,
onWillPop: () => Future.value(false),
);
}
/// Causes `AppLock` to either pop the [lockScreen] if the app is already running
/// or instantiates widget returned from the [builder] method if the app is cold
/// launched.
///
/// [args] is an optional argument which will get passed to the [builder] method
/// when built. Use this when you want to inject objects created from the
/// [lockScreen] in to the rest of your app so you can better guarantee that some
/// objects, services or databases are already instantiated before using them.
void didUnlock([Object? args]) {
if (this._didUnlockForAppLaunch) {
this._didUnlockOnAppPaused();
} else {
this._didUnlockOnAppLaunch(args);
}
}
/// Makes sure that [AppLock] shows the [lockScreen] on subsequent app pauses if
/// [enabled] is true of makes sure it isn't shown on subsequent app pauses if
/// [enabled] is false.
///
/// This is a convenience method for calling the [enable] or [disable] method based
/// on [enabled].
void setEnabled(bool enabled) {
if (enabled) {
this.enable();
} else {
this.disable();
}
}
/// Makes sure that [AppLock] shows the [lockScreen] on subsequent app pauses.
void enable() {
setState(() {
this._enabled = true;
});
}
/// Makes sure that [AppLock] doesn't show the [lockScreen] on subsequent app pauses.
void disable() {
setState(() {
this._enabled = false;
});
}
/// Manually show the [lockScreen].
Future<void> showLockScreen() {
this._isLocked = true;
return _navigatorKey.currentState!.pushNamed('/lock-screen');
}
void _didUnlockOnAppLaunch(Object? args) {
this._didUnlockForAppLaunch = true;
_navigatorKey.currentState!
.pushReplacementNamed('/unlocked', arguments: args);
}
void _didUnlockOnAppPaused() {
this._isLocked = false;
_navigatorKey.currentState!.pop();
}
}