Enable automatic update checks, download and install

- Enable update check with notification
- Enable update screen with releasenotes formatted from github markdown
- Update dependencies
This commit is contained in:
DJDoubleD
2024-07-31 20:21:21 +02:00
parent 31a359e5b6
commit 216d91be93
7 changed files with 268 additions and 113 deletions

View File

@@ -441,5 +441,8 @@ const language_en_us = {
'Open system settings': 'Open system settings',
'Application Log': 'Application Log',
'Are you sure you want to log out?': 'Are you sure you want to log out?',
// 0.7.13
'Download failed!': 'Download failed!',
}
};

View File

@@ -27,7 +27,7 @@ import 'ui/home_screen.dart';
import 'ui/library.dart';
import 'ui/login_screen.dart';
import 'ui/player_bar.dart';
//import 'ui/updater.dart';
import 'ui/updater.dart';
import 'ui/search.dart';
import 'utils/logging.dart';
import 'utils/navigator_keys.dart';
@@ -243,11 +243,9 @@ class _MainScreenState extends State<MainScreen>
_prepareQuickActions();
//Check for updates on background
/* No automatic updates yet
Future.delayed(Duration(seconds: 5), () {
FreezerVersions.checkUpdate();
Future.delayed(const Duration(seconds: 5), () {
ReFreezerLatest.checkUpdate();
});
*/
//Restore saved queue
_loadSavedQueue();

View File

@@ -143,7 +143,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => const UpdaterScreen())),
enabled: false,
),
ListTile(
title: Text('About'.i18n),

View File

@@ -2,10 +2,13 @@ import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:open_filex/open_filex.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
@@ -26,65 +29,113 @@ class UpdaterScreen extends StatefulWidget {
}
class _UpdaterScreenState extends State<UpdaterScreen> {
static const MethodChannel _platform = MethodChannel('r.r.refreezer/native');
bool _loading = true;
bool _error = false;
FreezerVersions? _versions;
String? _current;
ReFreezerLatest? _latestRelease;
Version _currentVersion = Version.parse('0.0.0');
String? _arch;
double _progress = 0.0;
bool _buttonEnabled = true;
static Future<bool> _checkInstallPackagesPermission() async {
try {
return await _platform.invokeMethod('checkInstallPackagesPermission');
} catch (e) {
Logger.root.severe('Failed to check install packages permission', e);
return false;
}
}
static Future<void> _requestInstallPackagesPermission() async {
try {
await _platform.invokeMethod('requestInstallPackagesPermission');
} catch (e) {
Logger.root.severe('Failed to request install packages permission', e);
}
}
Future _load() async {
//Load current version
// Load current version (convert DEBUG mode to SemVer)
PackageInfo info = await PackageInfo.fromPlatform();
setState(() => _current = info.version);
String versionString = info.version.replaceFirst(' DEBUG', '-debug');
// Parse the version string
setState(() {
_currentVersion =
Version.tryParse(versionString) ?? Version.parse('0.0.0');
});
//Get architecture
_arch = await DownloadManager.platform.invokeMethod('arch');
if (_arch == 'armv8l') _arch = 'arm32';
//Load from website
try {
FreezerVersions versions = await FreezerVersions.fetch();
ReFreezerLatest latestRelease = await ReFreezerLatest.fetch();
setState(() {
_versions = versions;
_latestRelease = latestRelease;
_loading = false;
});
} catch (e, st) {
if (kDebugMode) {
print(e.toString() + st.toString());
}
Logger.root.severe('Failed to load latest release', e, st);
_error = true;
_loading = false;
}
}
FreezerDownload? get _versionDownload {
return _versions?.versions[0].downloads.firstWhere((d) => d.version.toLowerCase().contains(_arch!.toLowerCase()));
ReFreezerDownload? get _versionDownload {
return _latestRelease?.downloads.firstWhereOrNull(
(d) => d.architectures
.any((arch) => arch.toLowerCase() == _arch?.toLowerCase()),
);
}
Future _download() async {
String? url = _versionDownload?.directUrl;
//Start request
http.Client client = http.Client();
http.StreamedResponse res = await client.send(http.Request('GET', Uri.parse(url ?? '')));
int? size = res.contentLength;
//Open file
String path = p.join((await getExternalStorageDirectory())!.path, 'update.apk');
File file = File(path);
IOSink fileSink = file.openWrite();
//Update progress
Future.doWhile(() async {
int received = await file.length();
setState(() => _progress = received / size!.toInt());
return received != size;
});
//Pipe
await res.stream.pipe(fileSink);
fileSink.close();
if (!await _checkInstallPackagesPermission()) {
await _requestInstallPackagesPermission();
}
OpenFilex.open(path);
setState(() => _buttonEnabled = true);
try {
String? url = _versionDownload?.directUrl;
if (url == null) {
throw Exception('No compatible download available');
}
//Start request
http.Client client = http.Client();
http.StreamedResponse res =
await client.send(http.Request('GET', Uri.parse(url)));
int? size = res.contentLength;
//Open file
String path =
p.join((await getExternalStorageDirectory())!.path, 'update.apk');
File file = File(path);
IOSink fileSink = file.openWrite();
//Update progress
Future.doWhile(() async {
int received = await file.length();
setState(() => _progress = received / size!.toInt());
return received != size;
});
//Pipe
await res.stream.pipe(fileSink);
fileSink.close();
OpenFilex.open(path);
setState(() {
_buttonEnabled = true;
_progress = 0.0;
});
} catch (e) {
Logger.root.severe('Failed to download latest release file', e);
Fluttertoast.showToast(
msg: 'Download failed!'.i18n,
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM);
setState(() {
_progress = 0.0;
_buttonEnabled = true;
});
}
}
@override
@@ -110,42 +161,60 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
),
if (!_error &&
!_loading &&
Version.parse((_versions?.latest.toString() ?? '0.0.0')) <= Version.parse(_current!))
(_latestRelease?.version ?? Version(0, 0, 0)) <=
_currentVersion)
Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('You are running latest version!'.i18n,
textAlign: TextAlign.center, style: const TextStyle(fontSize: 26.0)),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 26.0)),
)),
if (!_error &&
!_loading &&
Version.parse((_versions?.latest.toString() ?? '0.0.0')) > Version.parse(_current!))
(_latestRelease?.version ?? Version(0, 0, 0)) > _currentVersion)
Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'New update available!'.i18n + ' ' + _versions!.latest.toString(),
'New update available!'.i18n +
' ' +
_latestRelease!.version.toString(),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
Text(
'Current version: ' + _current!,
style: const TextStyle(fontSize: 14.0, fontStyle: FontStyle.italic),
'Current version: ' + _currentVersion.toString(),
style: const TextStyle(
fontSize: 14.0, fontStyle: FontStyle.italic),
),
Container(height: 8.0),
const FreezerDivider(),
Container(height: 8.0),
const Text(
'Changelog',
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
style:
TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
Container(height: 8.0),
const FreezerDivider(),
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: Text(
_versions!.versions[0].changelog,
/*child: Text(
_latestRelease?.changelog ?? '',
style: const TextStyle(fontSize: 16.0),
),*/
child: Container(
constraints: const BoxConstraints(
maxHeight: 350 // Screen Title height, ...
),
child: Markdown(
data: _latestRelease?.changelog ?? '',
shrinkWrap: true,
),
),
),
const FreezerDivider(),
@@ -154,16 +223,23 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
if (_versionDownload != null)
Column(children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: _buttonEnabled
? () {
setState(() => _buttonEnabled = false);
_download();
}
: null,
child: Text('Download'.i18n + ' (${_versionDownload?.version})')),
child: Text(
'Download'.i18n + ' (${_versionDownload?.abi})')),
Padding(
padding: const EdgeInsets.all(8.0),
child: LinearProgressIndicator(value: _progress),
child: LinearProgressIndicator(
value: _progress,
color: Theme.of(context).primaryColor,
),
)
]),
//Unsupported arch
@@ -180,79 +256,130 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
}
}
class FreezerVersions {
String latest;
List<FreezerVersion> versions;
class ReFreezerLatest {
final String versionString;
final Version version;
final String changelog;
final List<ReFreezerDownload> downloads;
FreezerVersions({required this.latest, required this.versions});
static const Map<String, List<String>> abiMap = {
'arm64-v8a': ['arm64', 'aarch64'],
'armeabi-v7a': ['arm32', 'armhf', 'armv8l'],
'x86_64': ['x86_64'],
};
factory FreezerVersions.fromJson(Map data) => FreezerVersions(
latest: data['android']['latest'],
versions: data['android']['versions'].map<FreezerVersion>((v) => FreezerVersion.fromJson(v)).toList());
ReFreezerLatest({
required this.versionString,
required this.changelog,
required this.downloads,
}) : version = Version.tryParse(versionString) ?? Version.parse('0.0.0');
//Fetch from website API
static Future<FreezerVersions> fetch() async {
http.Response response = await http.get('https://freezer.life/api/versions' as Uri);
// http.Response response = await http.get('https://cum.freezerapp.workers.dev/api/versions');
return FreezerVersions.fromJson(jsonDecode(response.body));
static Future<ReFreezerLatest> fetch() async {
http.Response res = await http.get(
Uri.parse(
'https://api.github.com/repos/DJDoubleD/refreezer/releases/latest'),
headers: {'Accept': 'application/vnd.github.v3+json'},
);
if (res.statusCode != 200) {
throw Exception(
'Failed to load latest version from Github API: $res.statusCode $res.statusMessage');
}
Map<String, dynamic> data = jsonDecode(res.body);
List<ReFreezerDownload> downloads = (data['assets'] as List)
.map((asset) {
String abi = abiMap.keys.firstWhere(
(key) => asset['name'].contains(key),
orElse: () => 'unknown',
);
return ReFreezerDownload(
abi: abi,
directUrl: asset['browser_download_url'],
);
})
.where((download) => download.abi != 'unknown')
.toList();
return ReFreezerLatest(
versionString: data['tag_name'],
changelog: data['body'],
downloads: downloads,
);
}
static Future checkUpdate() async {
//Check only each 24h
int updateDelay = 86400000;
if ((DateTime.now().millisecondsSinceEpoch - (cache.lastUpdateCheck ?? 0)) < updateDelay) return;
cache.lastUpdateCheck = DateTime.now().millisecondsSinceEpoch;
static Future<void> checkUpdate() async {
final now = DateTime.now().millisecondsSinceEpoch;
// Check every 24 hours
if (now - (cache.lastUpdateCheck ?? 0) <=
const Duration(hours: 24).inMilliseconds) {
return;
}
cache.lastUpdateCheck = now;
await cache.save();
FreezerVersions versions = await FreezerVersions.fetch();
try {
final latestVersion = await fetch();
//Load current version
PackageInfo info = await PackageInfo.fromPlatform();
if (Version.parse(versions.latest) <= Version.parse(info.version)) return;
//Load current version
final packageInfo = await PackageInfo.fromPlatform();
final currentVersion =
Version.tryParse(packageInfo.version) ?? Version.parse('0.0.0');
//Get architecture
String arch = await DownloadManager.platform.invokeMethod('arch');
if (arch == 'armv8l') arch = 'arm32';
//Check compatible architecture
var compatibleVersion =
versions.versions[0].downloads.firstWhereOrNull((d) => d.version.toLowerCase().contains(arch.toLowerCase()));
if (compatibleVersion == null) return;
if (latestVersion.version <= currentVersion) return;
//Show notification
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings androidInitializationSettings =
AndroidInitializationSettings('drawable/ic_logo');
const InitializationSettings initializationSettings =
InitializationSettings(android: androidInitializationSettings);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
'freezerupdates', 'Freezer Updates'.i18n,
channelDescription: 'Freezer Updates'.i18n);
NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails);
await flutterLocalNotificationsPlugin.show(
0, 'New update available!'.i18n, 'Update to latest version in the settings.'.i18n, notificationDetails);
//Get architecture
String arch = await DownloadManager.platform.invokeMethod('arch');
Logger.root
.info('Checking for updates to version $currentVersion on $arch');
if (!latestVersion.downloads.any((download) => download.architectures.any(
(architecture) =>
architecture.toLowerCase() == arch.toLowerCase()))) {
Logger.root.warning('No assets found for architecture $arch');
return;
}
//Show notification
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings androidInitializationSettings =
AndroidInitializationSettings('drawable/ic_logo');
const InitializationSettings initializationSettings =
InitializationSettings(
android: androidInitializationSettings, iOS: null);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'freezerupdates',
'Freezer Updates'.i18n,
channelDescription: 'Freezer Updates'.i18n,
importance: Importance.high,
priority: Priority.high,
);
NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails, iOS: null);
await flutterLocalNotificationsPlugin.show(
0,
'New update available!'.i18n,
'Update to latest version in the settings.'.i18n,
notificationDetails);
} catch (e) {
Logger.root.severe('Error checking for updates', e);
}
}
}
class FreezerVersion {
String version;
String changelog;
List<FreezerDownload> downloads;
class ReFreezerDownload {
final String abi;
final String directUrl;
final List<String> architectures;
FreezerVersion({required this.version, required this.changelog, required this.downloads});
factory FreezerVersion.fromJson(Map data) => FreezerVersion(
version: data['version'],
changelog: data['changelog'],
downloads: data['downloads'].map<FreezerDownload>((d) => FreezerDownload.fromJson(d)).toList());
}
class FreezerDownload {
String version;
String directUrl;
FreezerDownload({required this.version, required this.directUrl});
factory FreezerDownload.fromJson(Map data) =>
FreezerDownload(version: data['version'], directUrl: data['links'].first['url']);
ReFreezerDownload({required this.abi, required this.directUrl})
: architectures = ReFreezerLatest.abiMap[abi] ?? [abi];
}