From 216d91be93852007da8b86e6ab9fc129f5f1312e Mon Sep 17 00:00:00 2001
From: DJDoubleD <34967020+DJDoubleD@users.noreply.github.com>
Date: Wed, 31 Jul 2024 20:21:21 +0200
Subject: [PATCH] Enable automatic update checks, download and install - Enable
update check with notification - Enable update screen with releasenotes
formatted from github markdown - Update dependencies
---
android/app/src/main/AndroidManifest.xml | 1 +
.../main/java/r/r/refreezer/MainActivity.java | 26 ++
lib/languages/en_us.dart | 3 +
lib/main.dart | 8 +-
lib/ui/settings_screen.dart | 1 -
lib/ui/updater.dart | 339 ++++++++++++------
pubspec.yaml | 3 +-
7 files changed, 268 insertions(+), 113 deletions(-)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 30176ce..beb1de5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
+
diff --git a/android/app/src/main/java/r/r/refreezer/MainActivity.java b/android/app/src/main/java/r/r/refreezer/MainActivity.java
index b1f36e5..b0377cf 100644
--- a/android/app/src/main/java/r/r/refreezer/MainActivity.java
+++ b/android/app/src/main/java/r/r/refreezer/MainActivity.java
@@ -6,6 +6,7 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -15,6 +16,7 @@ import android.os.Message;
import android.os.Messenger;
import android.os.Parcelable;
import android.os.RemoteException;
+import android.provider.Settings;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -217,6 +219,17 @@ public class MainActivity extends AudioServiceActivity {
result.success(null);
return;
}
+ // Check if can request package install permission
+ if (call.method.equals("checkInstallPackagesPermission")) {
+ result.success(canRequestPackageInstalls());
+ return;
+ }
+ // Request package install permission
+ if (call.method.equals("requestInstallPackagesPermission")) {
+ requestInstallPackagesPermission();
+ result.success(true);
+ return;
+ }
result.error("0", "Not implemented!", "Not implemented!");
}));
@@ -236,6 +249,19 @@ public class MainActivity extends AudioServiceActivity {
}));
}
+ private boolean canRequestPackageInstalls() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return getPackageManager().canRequestPackageInstalls();
+ }
+ return true;
+ }
+
+ private void requestInstallPackagesPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startActivity(new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getPackageName())));
+ }
+ }
+
//Start/Bind/Reconnect to download service
private void connectService() {
if (serviceBound)
diff --git a/lib/languages/en_us.dart b/lib/languages/en_us.dart
index 72058a3..2bdda4b 100644
--- a/lib/languages/en_us.dart
+++ b/lib/languages/en_us.dart
@@ -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!',
}
};
diff --git a/lib/main.dart b/lib/main.dart
index c335d72..24af100 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -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
_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();
diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart
index dea82ed..83737cb 100644
--- a/lib/ui/settings_screen.dart
+++ b/lib/ui/settings_screen.dart
@@ -143,7 +143,6 @@ class _SettingsScreenState extends State {
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => const UpdaterScreen())),
- enabled: false,
),
ListTile(
title: Text('About'.i18n),
diff --git a/lib/ui/updater.dart b/lib/ui/updater.dart
index 97f9f28..4a4ca92 100644
--- a/lib/ui/updater.dart
+++ b/lib/ui/updater.dart
@@ -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 {
+ 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 _checkInstallPackagesPermission() async {
+ try {
+ return await _platform.invokeMethod('checkInstallPackagesPermission');
+ } catch (e) {
+ Logger.root.severe('Failed to check install packages permission', e);
+ return false;
+ }
+ }
+
+ static Future _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 {
),
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 {
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 {
}
}
-class FreezerVersions {
- String latest;
- List versions;
+class ReFreezerLatest {
+ final String versionString;
+ final Version version;
+ final String changelog;
+ final List downloads;
- FreezerVersions({required this.latest, required this.versions});
+ static const Map> 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((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 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 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 data = jsonDecode(res.body);
+
+ List 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 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 downloads;
+class ReFreezerDownload {
+ final String abi;
+ final String directUrl;
+ final List 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((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];
}
diff --git a/pubspec.yaml b/pubspec.yaml
index a316d93..8438a9d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -65,6 +65,7 @@ dependencies:
#flutter_file_dialog: ^3.0.1
flutter_inappwebview: ^6.0.0
flutter_local_notifications: ^17.0.0
+ flutter_markdown: ^0.7.3
flutter_material_color_picker: ^1.2.0
flutter_screenutil: ^5.9.0
get_it: ^7.6.4
@@ -113,7 +114,7 @@ dependencies:
rxdart: ^0.28.0
scrobblenaut:
path: ./scrobblenaut
- share_plus: ^9.0.0
+ share_plus: ^10.0.0
spotify: ^0.13.1
sqflite: ^2.3.0
url_launcher: ^6.2.2