From 5f239dbede2de0a5321633d8fa27272f613fb6b6 Mon Sep 17 00:00:00 2001 From: Alex Vinarskis Date: Fri, 29 Sep 2023 13:37:36 +0200 Subject: [PATCH] feat: integrate github api OTA 4linux --- lib/classes/ota_manager.dart | 96 +++++++++++++ lib/components/menu_ota.dart | 240 +++++++++++++++++++++++++++++++++ lib/configs/constants.dart | 14 +- lib/l10n/app_en.arb | 15 +++ lib/screens/screen_parent.dart | 7 +- pubspec.lock | 8 ++ pubspec.yaml | 1 + 7 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 lib/classes/ota_manager.dart create mode 100644 lib/components/menu_ota.dart diff --git a/lib/classes/ota_manager.dart b/lib/classes/ota_manager.dart new file mode 100644 index 0000000..0acd57b --- /dev/null +++ b/lib/classes/ota_manager.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:process_run/shell.dart'; +import 'package:version/version.dart'; + +import '../configs/constants.dart'; + +class OtaManager { + static final shell = Shell(); + + // [tagname, releaseUrl, downloadUrl] + static Future> checkLatestOta() async { + List result = []; + try { + if (Platform.isLinux) { + ProcessResult pr = (await shell.run('${Constants.githubApiRequest} ${Constants.githubApiReleases}'))[0]; + if (pr.exitCode != 0) { + return result; + } + Map json = jsonDecode(pr.stdout.toString()); + + // fetch tagname & url + if (!json.containsKey(Constants.githubApiFieldTagname)) { + return result; + } + result.add(json[Constants.githubApiFieldTagname]); + if (!json.containsKey(Constants.githubApiFieldHtmlUrl)) { + return result; + } + result.add(json[Constants.githubApiFieldHtmlUrl]); + + // fetch download url + if (!json.containsKey(Constants.githubApiFieldAssets)) { + return result; + } + for (Map asset in json[Constants.githubApiFieldAssets]) { + if (asset.containsKey(Constants.githubApiFieldBrowserDownloadUrl) && asset[Constants.githubApiFieldBrowserDownloadUrl].toString().endsWith('.deb')) { + result.add(asset[Constants.githubApiFieldBrowserDownloadUrl]); + break; + } + } + return result; + } else { + // ToDo Windows integration; + return result; + } + } catch (e) { + return result; + } + } + + static bool compareUpdateRequired(String tagname) { + Version currentVersion = Version.parse(Constants.applicationVersion.split('-')[0]); + Version latestVersion = Version.parse(tagname); + return latestVersion > currentVersion; + } + + static Future downloadOta(String tagname, String downloadUrl) async { + bool result = true; + try { + if (Platform.isLinux) { + List prs = (await shell.run(''' + rm -rf ${Constants.packagesLinuxDownloadPath}/* + mkdir -p ${Constants.packagesLinuxDownloadPath} + curl -L -A "User-Agent Mozilla" $downloadUrl -o ${Constants.packagesLinuxDownloadPath}/$tagname.deb + ''')); + for (ProcessResult pr in prs) { + result = pr.exitCode == 0 && result; + } + return result; + } else { + // ToDo Windows integration; + return false; + } + } catch (e) { + return false; + } + } + + static Future installOta(String tagname) async { + bool result = true; + try { + if (Platform.isLinux) { + ProcessResult pr = (await shell.run('pkexec bash -c "ss=0; apt install -y --allow-downgrades -f ${Constants.packagesLinuxDownloadPath}/$tagname.deb || ((ss++)); rm -rf ${Constants.packagesLinuxDownloadPath}/* || ((ss++)); exit \$ss"'))[0]; + result = pr.exitCode == 0; + return result; + } else { + // ToDo Windows integration; + return false; + } + } catch (e) { + return false; + } + } +} diff --git a/lib/components/menu_ota.dart b/lib/components/menu_ota.dart new file mode 100644 index 0000000..81bf059 --- /dev/null +++ b/lib/components/menu_ota.dart @@ -0,0 +1,240 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../classes/ota_manager.dart'; +import '../configs/constants.dart'; + +enum OtaState { + hidden, + awaiting, + downloading, + installing, + downloadFailed, + installationFailed, + installationSucceeded, +} + +class MenuOta extends StatefulWidget { + const MenuOta({super.key, this.paddingH = 0, this.paddingV = 0, this.backgroundColor = Colors.transparent}); + + final double paddingH; + final double paddingV; + final Color backgroundColor; + + @override + State createState() => MenuOtaState(); +} + +class MenuOtaState extends State { + // assume running latest version by default + OtaState _otaState = OtaState.hidden; + // [tagname, releaseUrl, downloadUrl] + List _targetVersion = []; + late Map otaStateTitles; + + @override + void initState() { + super.initState(); + OtaManager.checkLatestOta().then((latestOta) => _handleOtaState(latestOta)); + } + + void _handleOtaState(List latestOta) { + if (latestOta.length < 3) { + return; + } + if (!OtaManager.compareUpdateRequired(latestOta[0])) { + return; + } + if (_otaState == OtaState.hidden) { + setState(() { + _targetVersion = latestOta; + _otaState = OtaState.awaiting; + }); + } + } + + void _getOta() async { + setState(() { + _otaState = OtaState.downloading; + }); + bool downloaded = await OtaManager.downloadOta(_targetVersion[0], _targetVersion[2]); + if (!downloaded) { + setState(() { + _otaState = OtaState.downloadFailed; + }); + return; + } + setState(() { + _otaState = OtaState.installing; + }); + bool installed = await OtaManager.installOta(_targetVersion[0]); + setState(() { + if (installed) { + _otaState = OtaState.installationSucceeded; + } else { + _otaState = OtaState.installationFailed; + } + }); + } + + Widget _getProgressBar(var state, BuildContext context) { + switch (state) { + case OtaState.installing: + case OtaState.downloading: + return const LinearProgressIndicator(backgroundColor: Colors.transparent); + case OtaState.installationFailed: + case OtaState.downloadFailed: + return LinearProgressIndicator(backgroundColor: Colors.transparent, color: Theme.of(context).colorScheme.error, value: 1,); + case OtaState.installationSucceeded: + return const LinearProgressIndicator(backgroundColor: Colors.transparent, color: Colors.green, value: 1,); + default: + return const LinearProgressIndicator(backgroundColor: Colors.transparent, color: Colors.transparent,); + } + } + + Widget _getIcon(var state, BuildContext context) { + switch (state) { + case OtaState.installationFailed: + case OtaState.downloadFailed: + return Icon(Icons.error_outline_rounded, color: Theme.of(context).colorScheme.error,); + case OtaState.installationSucceeded: + return const Icon(Icons.check_circle_outline_outlined, color: Colors.green,); + default: + return const Icon(Icons.browser_updated_rounded); + } + } + + Future _showDownloadModal() { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(S.of(context)!.otaCardTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${S.of(context)!.otaAlertVersionCurrent}:\n${S.of(context)!.otaAlertVersionAvailable}:", + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + " ${Constants.applicationVersion}\n ${_targetVersion[0]}", + style: GoogleFonts.sourceCodePro().copyWith(color: Theme.of(context).textTheme.bodyMedium!.color!), + ), + ], + ), + Text( + "\n${S.of(context)!.otaAlertP1}", + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.justify, + ), + Text( + S.of(context)!.otaAlertP2, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.justify, + ), + ], + ), + actions: [ + TextButton.icon( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + icon: const Icon(Icons.link_rounded), + label: Text(S.of(context)!.otaAlertButtonRelease), + onPressed: () { + launchUrl(Uri.parse(_targetVersion[1])); + Navigator.of(context).pop(); + }, + ), + TextButton.icon( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + icon: const Icon(Icons.download_rounded), + label: Text(S.of(context)!.otaAlertButtonInstall), + onPressed: () { + _getOta(); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + otaStateTitles = { + OtaState.hidden : "", + OtaState.awaiting : S.of(context)!.otaCardSubtitleAwaiting, + OtaState.downloading : S.of(context)!.otaCardSubtitleDownloading, + OtaState.installing : S.of(context)!.otaCardSubtitleInstalling, + OtaState.downloadFailed : S.of(context)!.otaCardSubtitleDownloadFailed, + OtaState.installationFailed : S.of(context)!.otaCardSubtitleInstallationFailed, + OtaState.installationSucceeded : S.of(context)!.otaCardSubtitleInstallationSucceeded, + }; + + return AnimatedSwitcher( + duration: const Duration(milliseconds: Constants.animationMs), + child: _otaState != OtaState.hidden ? Card( + key: const Key("otaAvailableTrue"), + clipBehavior: Clip.antiAlias, + color: Colors.amber.withOpacity(0.4), + elevation: 0, + margin: EdgeInsets.symmetric(vertical: widget.paddingV, horizontal: widget.paddingH), + child: InkWell( + onTap: () async { + if (_otaState == OtaState.installing || _otaState == OtaState.downloading) { + return; + } + if (_otaState == OtaState.installationSucceeded) { + exit(0); + } + _showDownloadModal(); + }, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(left: 15, right: 15), + child: _getIcon(_otaState, context), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 15), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context)!.otaCardTitle, + style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 5,), + Text(otaStateTitles[_otaState].toString(), textAlign: TextAlign.justify,), + ], + ), + ), + ], + ), + Align(alignment: Alignment.bottomCenter, child: _getProgressBar(_otaState, context),), + ], + ), + ), + ) : const SizedBox( + key: Key("otaAvailableFalse"), + ), + ); + } +} diff --git a/lib/configs/constants.dart b/lib/configs/constants.dart index 5bd9db9..e71d7bb 100644 --- a/lib/configs/constants.dart +++ b/lib/configs/constants.dart @@ -2,8 +2,9 @@ class Constants { static const animationMs = 250; static const authorName = 'alexVinarskis'; - static const urlHomepage = 'https://github.com/alexVinarskis/dell-powermanager'; - static const urlBugReport = 'https://github.com/alexVinarskis/dell-powermanager/issues/new/choose'; + static const urlHomepage = 'https://github.com/${Constants.authorName}/dell-powermanager'; + static const urlBugReport = 'https://github.com/${Constants.authorName}/dell-powermanager/issues/new/choose'; + static const urlApi = 'https://api.github.com/repos/${Constants.authorName}/dell-powermanager'; static const packagesLinux = ['command-configure', 'srvadmin-hapi', 'libssl1.1']; static const packagesWindows = []; @@ -18,5 +19,12 @@ class Constants { // These string shall also be hardcoded to ./package! static const apiPathLinux = '/opt/dell/dcc/cctk'; static const applicationName = 'Dell Power Manager by VA'; - static const applicationVersion = '0.5.0-3-g99820b7+20230924-225519'; + static const applicationVersion = '0.1.0'; + + static const githubApiReleases = '${Constants.urlApi}/releases/latest'; + static const githubApiRequest = 'curl -L -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28"'; + static const githubApiFieldTagname = 'tag_name'; + static const githubApiFieldAssets = 'assets'; + static const githubApiFieldBrowserDownloadUrl = 'browser_download_url'; + static const githubApiFieldHtmlUrl = 'html_url'; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 51f8d96..1714293 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -57,6 +57,21 @@ "dependenciesAlertP2" : "Dell Command | Configure", "dependenciesAlertP3" : "CLI\nand its dependencies to operate. Press the button below\nto automatically install the following packages:\n\n", + "otaCardTitle" : "Update Available", + "otaCardSubtitleAwaiting" : "Tap for more info and install options", + "otaCardSubtitleDownloading" : "Please wait... Downloading...", + "otaCardSubtitleInstalling" : "Please wait... Installing...", + "otaCardSubtitleDownloadFailed" : "Download Failed :<", + "otaCardSubtitleInstallationFailed" : "Installation Failed :<", + "otaCardSubtitleInstallationSucceeded" : "Installation Succeeded. Please restart the app!", + "otaAlertTitle" : "Update Available", + "otaAlertButtonInstall" : "Download and Install", + "otaAlertButtonRelease" : "Release", + "otaAlertVersionCurrent" : "Current Version", + "otaAlertVersionAvailable" : "Available Version", + "otaAlertP1" : "Find more information and source code on the release page.", + "otaAlertP2" : "Press the button below to automatically install the update.", + "cctkThermalOptimizedTitle" : "Optimized", "cctkThermalOptimizedDescription" : "This is the standard setting for cooling fan and processor heat management. This setting is a balance of performance., noise and temperature.", "cctkThermalQuietTitle" : "Quiet", diff --git a/lib/screens/screen_parent.dart b/lib/screens/screen_parent.dart index 16a94d5..a14342c 100644 --- a/lib/screens/screen_parent.dart +++ b/lib/screens/screen_parent.dart @@ -3,6 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../components/menu_item.dart'; import '../components/info_button.dart'; import '../components/menu_dependencies.dart'; +import '../components/menu_ota.dart'; import '../configs/constants.dart'; import '../screens/screen_battery.dart'; import '../screens/screen_summary.dart'; @@ -87,7 +88,11 @@ class ScreenParentState extends State { ), const Expanded(child: SizedBox(),), const MenuDependencies( - paddingV: 0, + paddingV: 10, + paddingH: 20, + ), + const MenuOta( + paddingV: 10, paddingH: 20, ), InfoButton( diff --git a/pubspec.lock b/pubspec.lock index 824ad48..3580c82 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -562,6 +562,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + version: + dependency: "direct main" + description: + name: version + sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" + url: "https://pub.dev" + source: hosted + version: "3.0.2" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 22e5457..9f62262 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: skeleton_text: ^3.0.1 intl: ^0.18.1 flutter_localization: ^0.1.14 + version: ^3.0.2 dev_dependencies: flutter_test: