Selaa lähdekoodia

Show lockscreen onStart and onResume

Vishnu Mohandas 4 vuotta sitten
vanhempi
commit
bf09a87997

+ 6 - 0
ios/Podfile.lock

@@ -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

+ 2 - 0
ios/Runner.xcodeproj/project.pbxproj

@@ -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",

+ 49 - 38
lib/main.dart

@@ -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,
+    );
+  }
+
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    if (state == AppLifecycleState.resumed) {
+      _logger.info("App resumed");
+      _sync();
+    }
+  }
 
-    // Configure BackgroundFetch.
+  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 - 0
lib/ui/app_lock.dart

@@ -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();
+  }
+}

+ 55 - 38
lib/ui/lock_screen.dart

@@ -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;
+      });
+    }
+  }
 }

+ 6 - 1
lib/ui/settings_page.dart

@@ -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());
                     }
                   },
                 ),

+ 2 - 0
lib/utils/auth_util.dart

@@ -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(