Show lockscreen onStart and onResume

This commit is contained in:
Vishnu Mohandas 2021-03-21 14:02:10 +05:30
parent 80d62620a1
commit bf09a87997
7 changed files with 296 additions and 77 deletions

View file

@ -44,6 +44,8 @@ PODS:
- libwebp/mux (1.1.0):
- libwebp/demux
- libwebp/webp (1.1.0)
- local_auth (0.0.1):
- Flutter
- Mantle (2.1.1):
- Mantle/extobjc (= 2.1.1)
- Mantle/extobjc (2.1.1)
@ -91,6 +93,7 @@ DEPENDENCIES:
- fluttercontactpicker (from `.symlinks/plugins/fluttercontactpicker/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- in_app_purchase (from `.symlinks/plugins/in_app_purchase/ios`)
- local_auth (from `.symlinks/plugins/local_auth/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
@ -143,6 +146,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios"
in_app_purchase:
:path: ".symlinks/plugins/in_app_purchase/ios"
local_auth:
:path: ".symlinks/plugins/local_auth/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider:
@ -182,6 +187,7 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6
libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3
local_auth: 25938960984c3a7f6e3253e3f8d962fdd16852bd
Mantle: 35238ae6f2e2b2d474fa7b67fee82a59fea71915
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c

View file

@ -273,6 +273,7 @@
"${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework",
"${BUILT_PRODUCTS_DIR}/in_app_purchase/in_app_purchase.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
"${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework",
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
"${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework",
@ -307,6 +308,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework",

View file

@ -11,7 +11,9 @@ import 'package:photos/services/billing_service.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/memories_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/app_lock.dart';
import 'package:photos/ui/home_widget.dart';
import 'package:photos/ui/lock_screen.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_uploader.dart';
import 'package:super_logging/super_logging.dart';
@ -21,6 +23,27 @@ final _logger = Logger("main");
Completer<void> _initializationStatus;
final themeData = ThemeData(
fontFamily: 'Ubuntu',
brightness: Brightness.dark,
hintColor: Colors.grey,
accentColor: Color.fromRGBO(45, 194, 98, 1.0),
buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
buttonTheme: ButtonThemeData().copyWith(
buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
),
toggleableActiveColor: Colors.green[400],
scaffoldBackgroundColor: Colors.black,
backgroundColor: Colors.black,
appBarTheme: AppBarTheme().copyWith(
color: Color.fromRGBO(10, 20, 20, 1.0),
),
cardColor: Color.fromRGBO(25, 25, 25, 1.0),
dialogTheme: DialogTheme().copyWith(
backgroundColor: Color.fromRGBO(20, 20, 20, 1.0),
),
);
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await _runInForeground();
@ -32,7 +55,12 @@ Future<void> _runInForeground() async {
_logger.info("Starting app in foreground");
await _init(false);
_sync();
runApp(MyApp());
runApp(AppLock(
builder: (args) => EnteApp(),
lockScreen: LockScreen(),
enabled: Configuration.instance.shouldShowLockScreen(),
themeData: themeData,
));
});
}
@ -104,13 +132,30 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
));
}
class MyApp extends StatelessWidget with WidgetsBindingObserver {
final _title = 'ente';
class EnteApp extends StatelessWidget with WidgetsBindingObserver {
static const _homeWidget = const HomeWidget();
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addObserver(this);
_configureBackgroundFetch();
return MaterialApp(
title: "ente",
theme: themeData,
home: _homeWidget,
debugShowCheckedModeBanner: false,
);
}
// Configure BackgroundFetch.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_logger.info("App resumed");
_sync();
}
}
void _configureBackgroundFetch() {
BackgroundFetch.configure(
BackgroundFetchConfig(
minimumFetchInterval: 15,
@ -130,39 +175,5 @@ class MyApp extends StatelessWidget with WidgetsBindingObserver {
}).catchError((e) {
_logger.info('[BackgroundFetch] configure ERROR: $e');
});
return MaterialApp(
title: _title,
theme: ThemeData(
fontFamily: 'Ubuntu',
brightness: Brightness.dark,
hintColor: Colors.grey,
accentColor: Color.fromRGBO(45, 194, 98, 1.0),
buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
buttonTheme: ButtonThemeData().copyWith(
buttonColor: Color.fromRGBO(45, 194, 98, 1.0),
),
toggleableActiveColor: Colors.green[400],
scaffoldBackgroundColor: Colors.black,
backgroundColor: Colors.black,
appBarTheme: AppBarTheme().copyWith(
color: Color.fromRGBO(10, 20, 20, 1.0),
),
cardColor: Color.fromRGBO(25, 25, 25, 1.0),
dialogTheme: DialogTheme().copyWith(
backgroundColor: Color.fromRGBO(20, 20, 20, 1.0),
),
),
home: HomeWidget(_title),
debugShowCheckedModeBanner: false,
);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_logger.info("App resumed");
_sync();
}
}
}

176
lib/ui/app_lock.dart Normal file
View file

@ -0,0 +1,176 @@
import 'dart:async';
import 'package:flutter/material.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.
class AppLock extends StatefulWidget {
final Widget Function(Object) builder;
final Widget lockScreen;
final bool enabled;
final Duration backgroundLockLatency;
final ThemeData themeData;
const AppLock({
Key key,
@required this.builder,
@required this.lockScreen,
this.enabled = true,
this.backgroundLockLatency = const Duration(seconds: 0),
this.themeData,
}) : super(key: key);
static _AppLockState of(BuildContext context) =>
context.findAncestorStateOfType<_AppLockState>();
@override
_AppLockState createState() => _AppLockState();
}
class _AppLockState extends State<AppLock> with WidgetsBindingObserver {
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
bool _didUnlockForAppLaunch;
bool _isLocked;
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,
theme: widget.themeData,
routes: {
'/lock-screen': (context) => this._lockScreen,
'/unlocked': (context) =>
this.widget.builder(ModalRoute.of(context).settings.arguments)
},
);
}
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();
}
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/ui/home_widget.dart';
import 'package:logging/logging.dart';
import 'package:photos/ui/app_lock.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/utils/auth_util.dart';
@ -12,52 +13,68 @@ class LockScreen extends StatefulWidget {
}
class _LockScreenState extends State<LockScreen> {
bool _isUnlocking = false;
final _logger = Logger("LockScreen");
bool _isUnlocking = true;
@override
void initState() {
_showLockScreen();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: double.infinity,
height: 64,
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
child: _isUnlocking
? Padding(
padding: const EdgeInsets.only(top: 24),
child: loadWidget,
)
: RaisedButton(
child: Text(
"unlock",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
letterSpacing: 1.0,
width: double.infinity,
height: 64,
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
child: _isUnlocking
? Padding(
padding: const EdgeInsets.only(top: 24),
child: loadWidget,
)
: RaisedButton(
child: Text(
"unlock",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
letterSpacing: 1.0,
),
textAlign: TextAlign.center,
),
textAlign: TextAlign.center,
),
onPressed: () async {
setState(() {
_isUnlocking = true;
});
final result = await requestAuthentication();
if (result) {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (_) {
return HomeWidget();
}));
} else {
onPressed: () async {
setState(() {
_isUnlocking = false;
_isUnlocking = true;
});
}
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
),
_showLockScreen();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
)),
),
);
}
Future<void> _showLockScreen() async {
_logger.info("Showing lockscreen");
try {
final result = await requestAuthentication();
if (result) {
AppLock.of(context).didUnlock();
} else {
setState(() {
_isUnlocking = false;
});
}
} catch (e) {
_logger.severe(e);
setState(() {
_isUnlocking = false;
});
}
}
}

View file

@ -7,9 +7,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photos/ui/app_lock.dart';
import 'package:photos/utils/auth_util.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
@ -284,10 +284,15 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
Switch(
value: Configuration.instance.shouldShowLockScreen(),
onChanged: (value) async {
AppLock.of(context).disable();
final result = await requestAuthentication();
if (result) {
AppLock.of(context).setEnabled(value);
Configuration.instance.setShouldShowLockScreen(value);
setState(() {});
} else {
AppLock.of(context).setEnabled(
Configuration.instance.shouldShowLockScreen());
}
},
),

View file

@ -1,7 +1,9 @@
import 'package:local_auth/auth_strings.dart';
import 'package:local_auth/local_auth.dart';
import 'package:logging/logging.dart';
Future<bool> requestAuthentication() async {
Logger("AuthUtil").info("Requesting authentication");
return await LocalAuthentication().authenticate(
localizedReason: "please authenticate to view your memories",
androidAuthStrings: AndroidAuthMessages(