diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart new file mode 100644 index 000000000..46c61be4f --- /dev/null +++ b/lib/services/update_service.dart @@ -0,0 +1,61 @@ +import 'dart:io'; + +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:photos/core/network.dart'; + +class UpdateService { + UpdateService._privateConstructor(); + static final UpdateService instance = UpdateService._privateConstructor(); + + LatestVersionInfo _latestVersion; + + Future shouldUpdate() async { + if (Platform.isIOS) { + return false; + } + _latestVersion = await _getLatestVersionInfo(); + final currentVersionCode = + int.parse((await PackageInfo.fromPlatform()).buildNumber); + return currentVersionCode < _latestVersion.code; + } + + LatestVersionInfo getLatestVersionInfo() { + return _latestVersion; + } + + Future _getLatestVersionInfo() async { + final response = await Network.instance + .getDio() + .get("https://android.ente.io/release-info.json"); + return LatestVersionInfo.fromMap(response.data["latestVersion"]); + } +} + +class LatestVersionInfo { + final String name; + final int code; + final List changelog; + final bool shouldForceUpdate; + final String url; + final int size; + + LatestVersionInfo( + this.name, + this.code, + this.changelog, + this.shouldForceUpdate, + this.url, + this.size, + ); + + factory LatestVersionInfo.fromMap(Map map) { + return LatestVersionInfo( + map['name'], + map['code'], + List.from(map['changelog']), + map['shouldForceUpdate'], + map['url'], + map['size'], + ); + } +} diff --git a/lib/ui/app_update_dialog.dart b/lib/ui/app_update_dialog.dart new file mode 100644 index 000000000..fd6559d51 --- /dev/null +++ b/lib/ui/app_update_dialog.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; +import 'package:open_file/open_file.dart'; +import 'package:photos/core/configuration.dart'; +import 'package:photos/core/network.dart'; +import 'package:photos/services/update_service.dart'; +import 'package:photos/ui/common_elements.dart'; +import 'package:photos/utils/dialog_util.dart'; + +class AppUpdateDialog extends StatefulWidget { + final LatestVersionInfo latestVersionInfo; + + AppUpdateDialog(this.latestVersionInfo, {Key key}) : super(key: key); + + @override + _AppUpdateDialogState createState() => _AppUpdateDialogState(); +} + +class _AppUpdateDialogState extends State { + @override + Widget build(BuildContext context) { + final List changelog = []; + for (final log in widget.latestVersionInfo.changelog) { + changelog.add(Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 0, 4), + child: Text("- " + log, + style: TextStyle( + fontSize: 14, + color: Colors.white.withOpacity(0.7), + )), + )); + } + final content = Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.latestVersionInfo.name, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Padding(padding: EdgeInsets.all(8)), + Text("changelog", + style: TextStyle( + fontSize: 18, + )), + Padding(padding: EdgeInsets.all(4)), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: changelog, + ), + Padding(padding: EdgeInsets.all(8)), + Container( + width: double.infinity, + height: 64, + padding: const EdgeInsets.fromLTRB(64, 0, 64, 0), + child: button( + "update", + fontSize: 16, + onPressed: () async { + Navigator.pop(context); + showDialog( + context: context, + builder: (BuildContext context) { + return ApkDownloaderDialog(widget.latestVersionInfo); + }, + barrierDismissible: false, + ); + }, + ), + ), + ], + ); + return WillPopScope( + onWillPop: () async => !widget.latestVersionInfo.shouldForceUpdate, + child: AlertDialog( + title: Text("update available"), + content: content, + ), + ); + } +} + +class ApkDownloaderDialog extends StatefulWidget { + final LatestVersionInfo versionInfo; + + ApkDownloaderDialog(this.versionInfo, {Key key}) : super(key: key); + + @override + _ApkDownloaderDialogState createState() => _ApkDownloaderDialogState(); +} + +class _ApkDownloaderDialogState extends State { + String _saveUrl; + double _downloadProgress; + + @override + void initState() { + super.initState(); + _saveUrl = Configuration.instance.getTempDirectory() + + "ente-" + + widget.versionInfo.name + + ".apk"; + _downloadApk(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: AlertDialog( + title: Text( + "downloading...", + style: TextStyle( + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + content: LinearProgressIndicator( + value: _downloadProgress, + valueColor: + AlwaysStoppedAnimation(Theme.of(context).buttonColor), + ), + ), + ); + } + + Future _downloadApk() async { + try { + await Network.instance.getDio().download(widget.versionInfo.url, _saveUrl, + onReceiveProgress: (count, _) { + setState(() { + _downloadProgress = count / widget.versionInfo.size; + }); + }); + } catch (e) { + Logger("ApkDownloader").severe(e); + Navigator.pop(context); + showGenericErrorDialog(context); + return; + } + Navigator.pop(context); + OpenFile.open(_saveUrl); + } +} diff --git a/lib/ui/home_widget.dart b/lib/ui/home_widget.dart index 00d6b6f7c..8c2d9d4a8 100644 --- a/lib/ui/home_widget.dart +++ b/lib/ui/home_widget.dart @@ -17,6 +17,8 @@ import 'package:photos/events/trigger_logout_event.dart'; import 'package:photos/events/user_logged_out_event.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/services/sync_service.dart'; +import 'package:photos/services/update_service.dart'; +import 'package:photos/ui/app_update_dialog.dart'; import 'package:photos/ui/backup_folder_selection_page.dart'; import 'package:photos/ui/collections_gallery_widget.dart'; import 'package:photos/ui/extents_page_view.dart'; @@ -126,6 +128,18 @@ class _HomeWidgetState extends State { setState(() {}); }); _initDeepLinks(); + UpdateService.instance.shouldUpdate().then((shouldUpdate) { + if (shouldUpdate) { + Future.delayed(Duration.zero, () { + showDialog( + context: context, + builder: (BuildContext context) { + return AppUpdateDialog( + UpdateService.instance.getLatestVersionInfo()); + }); + }); + } + }); super.initState(); } diff --git a/pubspec.lock b/pubspec.lock index e632b3080..d4fa4744e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -478,6 +478,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0+1" + open_file: + dependency: "direct main" + description: + name: open_file + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" package_info_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 937e27b17..3875634b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dependencies: loading_animations: ^2.1.0 dots_indicator: ^2.0.0 flutter_local_notifications: ^5.0.0+4 + open_file: ^3.2.1 dev_dependencies: flutter_test: