Added updater for GitHub downloads

This commit is contained in:
Juan Gilsanz Polo 2022-10-29 18:40:07 +02:00
parent 779fe25ef7
commit fbe364ad25
17 changed files with 733 additions and 6 deletions

View file

@ -39,6 +39,10 @@ This is an unofficial application. The AdGuard Home team and the development of
- [flutter svg](https://pub.dev/packages/flutter_svg)
- [bottom sheet](https://pub.dev/packages/bottom_sheet)
- [percent indicator](https://pub.dev/packages/percent_indicator)
- [store checker](https://pub.dev/packages/store_checker)
- [fl downloader](https://pub.dev/packages/fl_downloader)
- [install plugin v2](https://pub.dev/packages/install_plugin_v2)
- [permission handler](https://pub.dev/packages/permission_handler)
<br>

View file

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 33
ndkVersion flutter.ndkVersion
compileOptions {

View file

@ -5,4 +5,9 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="33" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
</manifest>

View file

@ -1,9 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jgeek00.adguard_home_manager">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="33" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
<application
android:label="AdGuard Home Manager"
android:name="${applicationName}"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"

View file

@ -5,4 +5,9 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="33" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
</manifest>

View file

@ -1,25 +1,137 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:install_plugin_v2/install_plugin_v2.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:store_checker/store_checker.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart';
import 'package:adguard_home_manager/widgets/update_modal.dart';
import 'package:adguard_home_manager/widgets/download_modal.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/models/github_release.dart';
import 'package:adguard_home_manager/constants/package_name.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/models/app_screen.dart';
import 'package:adguard_home_manager/config/app_screens.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class Base extends StatefulWidget {
const Base({Key? key}) : super(key: key);
final AppConfigProvider appConfigProvider;
const Base({
Key? key,
required this.appConfigProvider,
}) : super(key: key);
@override
State<Base> createState() => _BaseState();
}
class _BaseState extends State<Base> {
class _BaseState extends State<Base> with WidgetsBindingObserver {
int selectedScreen = 0;
Future<GitHubRelease?> checkInstallationSource() async {
Source installationSource = await StoreChecker.getSource;
if (installationSource != Source.IS_INSTALLED_FROM_PLAY_STORE) {
final result = await checkAppUpdatesGitHub();
if (result['result'] == 'success') {
if (result['body'].tagName != widget.appConfigProvider.getAppInfo!.version) {
return result['body'];
}
}
}
return null;
}
Future<bool> managePermission() async {
try {
if (await Permission.storage.isGranted) {
return true;
}
else {
final PermissionStatus status = await Permission.storage.request();
if (status.isGranted == false) {
return false;
}
else {
return true;
}
}
} catch (e) {
return false;
}
}
void installApk(String path) async {
final granted = await managePermission();
if (granted == true) {
path = path.replaceFirst(r'file://', '');
final file = File(path);
final exists = await file.exists();
if (exists) {
InstallPlugin.installApk(
path, PackageName.packageName
);
}
}
}
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
void download(String link, String version) async {
final granted = await managePermission();
if (granted == true) {
showDialog(
context: context,
builder: (context) => DownloadModal(
url: link,
version: version,
onFinish: installApk,
),
barrierDismissible: false
);
}
else {
showSnacbkar(
context: context,
appConfigProvider: widget.appConfigProvider,
label: AppLocalizations.of(context)!.permissionNotGranted,
color: Colors.red
);
}
}
final result = await checkInstallationSource();
if (result != null) {
await showDialog(
context: context,
builder: (context) => UpdateModal(
gitHubRelease: result,
onDownload: download,
),
);
}
});
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);

View file

@ -0,0 +1,3 @@
class PackageName {
static const String packageName = "com.jgeek00.adguard_home_manager";
}

View file

@ -2,4 +2,5 @@ class Urls {
static const String playStore = "https://play.google.com/store/apps/details?id=com.jgeek00.adguard_home_manager";
static const String gitHub = "https://github.com/JGeek00/adguard-home-manager";
static const String customRuleDocs = "https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters";
static const String checkLatestReleaseUrl = "https://api.github.com/repos/JGeek00/adguard-home-manager/releases/latest";
}

View file

@ -559,5 +559,15 @@
"nameTimeLogs": "Name and time on logs",
"nameTimeLogsDescription": "Show client name and processing time on logs list",
"hostNames": "Host names",
"keyType": "Key type"
"keyType": "Key type",
"updateAvailable": "Update available",
"installedVersion": "Installed version",
"newVersion": "New version",
"source": "Source",
"downloadUpdate": "Download update",
"download": "Download",
"doNotRememberAgainUpdate": "Do not remember again for this version",
"downloadingUpdate": "Downloading",
"completed": "completed",
"permissionNotGranted": "Permission not granted"
}

View file

@ -559,5 +559,15 @@
"nameTimeLogs": "Nombre y tiempo en logs",
"nameTimeLogsDescription": "Mostrar el nombre del cliente y el tiempo de procesamiento en la lista de logs",
"hostNames": "Nombres de host",
"keyType": "Tipo de clave"
"keyType": "Tipo de clave",
"updateAvailable": "Actualización disponible",
"installedVersion": "Versión instalada",
"newVersion": "Nueva versión",
"source": "Fuente",
"downloadUpdate": "Descargar actualización",
"download": "Descargar",
"doNotRememberAgainUpdate": "No recordar de nuevo para esta actualización",
"downloadingUpdate": "Descargando",
"completed": "completado",
"permissionNotGranted": "Permiso no concedido"
}

View file

@ -137,7 +137,7 @@ class _MainState extends State<Main> {
child: child!,
);
},
home: const Base(),
home: Base(appConfigProvider: appConfigProvider),
),
);
}

View file

@ -0,0 +1,232 @@
class GitHubRelease {
final String url;
final String assetsUrl;
final String uploadUrl;
final String htmlUrl;
final int id;
final Author author;
final String nodeId;
final String tagName;
final String targetCommitish;
final String name;
final bool draft;
final bool prerelease;
final DateTime createdAt;
final DateTime publishedAt;
final List<Asset> assets;
final String tarballUrl;
final String zipballUrl;
final String body;
GitHubRelease({
required this.url,
required this.assetsUrl,
required this.uploadUrl,
required this.htmlUrl,
required this.id,
required this.author,
required this.nodeId,
required this.tagName,
required this.targetCommitish,
required this.name,
required this.draft,
required this.prerelease,
required this.createdAt,
required this.publishedAt,
required this.assets,
required this.tarballUrl,
required this.zipballUrl,
required this.body,
});
factory GitHubRelease.fromJson(Map<String, dynamic> json) => GitHubRelease(
url: json["url"],
assetsUrl: json["assets_url"],
uploadUrl: json["upload_url"],
htmlUrl: json["html_url"],
id: json["id"],
author: Author.fromJson(json["author"]),
nodeId: json["node_id"],
tagName: json["tag_name"],
targetCommitish: json["target_commitish"],
name: json["name"],
draft: json["draft"],
prerelease: json["prerelease"],
createdAt: DateTime.parse(json["created_at"]),
publishedAt: DateTime.parse(json["published_at"]),
assets: List<Asset>.from(json["assets"].map((x) => Asset.fromJson(x))),
tarballUrl: json["tarball_url"],
zipballUrl: json["zipball_url"],
body: json["body"],
);
Map<String, dynamic> toJson() => {
"url": url,
"assets_url": assetsUrl,
"upload_url": uploadUrl,
"html_url": htmlUrl,
"id": id,
"author": author.toJson(),
"node_id": nodeId,
"tag_name": tagName,
"target_commitish": targetCommitish,
"name": name,
"draft": draft,
"prerelease": prerelease,
"created_at": createdAt.toIso8601String(),
"published_at": publishedAt.toIso8601String(),
"assets": List<dynamic>.from(assets.map((x) => x.toJson())),
"tarball_url": tarballUrl,
"zipball_url": zipballUrl,
"body": body,
};
}
class Asset {
final String url;
final int id;
final String nodeId;
final String name;
final dynamic label;
final Author uploader;
final String contentType;
final String state;
final int size;
final int downloadCount;
final DateTime createdAt;
final DateTime updatedAt;
final String browserDownloadUrl;
Asset({
required this.url,
required this.id,
required this.nodeId,
required this.name,
required this.label,
required this.uploader,
required this.contentType,
required this.state,
required this.size,
required this.downloadCount,
required this.createdAt,
required this.updatedAt,
required this.browserDownloadUrl,
});
factory Asset.fromJson(Map<String, dynamic> json) => Asset(
url: json["url"],
id: json["id"],
nodeId: json["node_id"],
name: json["name"],
label: json["label"],
uploader: Author.fromJson(json["uploader"]),
contentType: json["content_type"],
state: json["state"],
size: json["size"],
downloadCount: json["download_count"],
createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]),
browserDownloadUrl: json["browser_download_url"],
);
Map<String, dynamic> toJson() => {
"url": url,
"id": id,
"node_id": nodeId,
"name": name,
"label": label,
"uploader": uploader.toJson(),
"content_type": contentType,
"state": state,
"size": size,
"download_count": downloadCount,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
"browser_download_url": browserDownloadUrl,
};
}
class Author {
final String login;
final int id;
final String nodeId;
final String avatarUrl;
final String gravatarId;
final String url;
final String htmlUrl;
final String followersUrl;
final String followingUrl;
final String gistsUrl;
final String starredUrl;
final String subscriptionsUrl;
final String organizationsUrl;
final String reposUrl;
final String eventsUrl;
final String receivedEventsUrl;
final String type;
final bool siteAdmin;
Author({
required this.login,
required this.id,
required this.nodeId,
required this.avatarUrl,
required this.gravatarId,
required this.url,
required this.htmlUrl,
required this.followersUrl,
required this.followingUrl,
required this.gistsUrl,
required this.starredUrl,
required this.subscriptionsUrl,
required this.organizationsUrl,
required this.reposUrl,
required this.eventsUrl,
required this.receivedEventsUrl,
required this.type,
required this.siteAdmin,
});
factory Author.fromJson(Map<String, dynamic> json) => Author(
login: json["login"],
id: json["id"],
nodeId: json["node_id"],
avatarUrl: json["avatar_url"],
gravatarId: json["gravatar_id"],
url: json["url"],
htmlUrl: json["html_url"],
followersUrl: json["followers_url"],
followingUrl: json["following_url"],
gistsUrl: json["gists_url"],
starredUrl: json["starred_url"],
subscriptionsUrl: json["subscriptions_url"],
organizationsUrl: json["organizations_url"],
reposUrl: json["repos_url"],
eventsUrl: json["events_url"],
receivedEventsUrl: json["received_events_url"],
type: json["type"],
siteAdmin: json["site_admin"],
);
Map<String, dynamic> toJson() => {
"login": login,
"id": id,
"node_id": nodeId,
"avatar_url": avatarUrl,
"gravatar_id": gravatarId,
"url": url,
"html_url": htmlUrl,
"followers_url": followersUrl,
"following_url": followingUrl,
"gists_url": gistsUrl,
"starred_url": starredUrl,
"subscriptions_url": subscriptionsUrl,
"organizations_url": organizationsUrl,
"repos_url": reposUrl,
"events_url": eventsUrl,
"received_events_url": receivedEventsUrl,
"type": type,
"site_admin": siteAdmin,
};
}

View file

@ -8,6 +8,7 @@ import 'package:adguard_home_manager/models/dhcp.dart';
import 'package:adguard_home_manager/models/dns_info.dart';
import 'package:adguard_home_manager/models/encryption.dart';
import 'package:adguard_home_manager/models/filtering.dart';
import 'package:adguard_home_manager/models/github_release.dart';
import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/models/filtering_status.dart';
import 'package:adguard_home_manager/models/app_log.dart';
@ -17,6 +18,7 @@ import 'package:adguard_home_manager/models/server_status.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/models/clients_allowed_blocked.dart';
import 'package:adguard_home_manager/models/server.dart';
import 'package:adguard_home_manager/constants/urls.dart';
Future<Map<String, dynamic>> apiRequest({
@ -1812,3 +1814,74 @@ Future saveEncryptionSettings({
return result;
}
}
Future checkAppUpdatesGitHub() async {
try {
HttpClient httpClient = HttpClient();
HttpClientRequest request = await httpClient.getUrl(Uri.parse(Urls.checkLatestReleaseUrl));
HttpClientResponse response = await request.close();
String reply = await response.transform(utf8.decoder).join();
httpClient.close();
if (response.statusCode == 200) {
return {
'result': 'success',
'hasResponse': true,
'error': false,
'statusCode': response.statusCode,
'body': GitHubRelease.fromJson(jsonDecode(reply))
};
}
else {
return {
'result': 'error',
'log': AppLog(
type: 'update_encryption_settings',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
statusCode: response.statusCode.toString(),
resBody: reply,
)
};
}
} on SocketException {
return {
'result': 'no_connection',
'message': 'SocketException',
'log': AppLog(
type: 'check_latest_release_github',
dateTime: DateTime.now(),
message: 'SocketException'
)
};
} on TimeoutException {
return {
'result': 'no_connection',
'message': 'TimeoutException',
'log': AppLog(
type: 'check_latest_release_github',
dateTime: DateTime.now(),
message: 'TimeoutException'
)
};
} on HandshakeException {
return {
'result': 'ssl_error',
'message': 'HandshakeException',
'log': AppLog(
type: 'check_latest_release_github',
dateTime: DateTime.now(),
message: 'HandshakeException'
)
};
} catch (e) {
return {
'result': 'error',
'message': e.toString(),
'log': AppLog(
type: 'check_latest_release_github',
dateTime: DateTime.now(),
message: e.toString()
)
};
}
}

View file

@ -0,0 +1,107 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'package:fl_downloader/fl_downloader.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class DownloadModal extends StatefulWidget {
final String url;
final String version;
final void Function(String) onFinish;
const DownloadModal({
Key? key,
required this.url,
required this.version,
required this.onFinish,
}) : super(key: key);
@override
State<DownloadModal> createState() => _DownloadModalState();
}
class _DownloadModalState extends State<DownloadModal> {
int progress = 0;
late StreamSubscription progressStream;
void download() async {
final downloads = Directory('/storage/emulated/0/Download').listSync();
final installers = downloads.where((file) => file.path.contains('adguard-home-manager_v'));
try {
for (FileSystemEntity installer in installers) {
if (await installer.exists()) {
await installer.delete();
}
}
FlDownloader.initialize();
progressStream = FlDownloader.progressStream.listen((event) {
if (event.status == DownloadStatus.successful) {
setState(() => progress = event.progress);
Navigator.pop(context);
widget.onFinish(event.filePath!);
}
else if (event.status == DownloadStatus.running) {
setState(() => progress = event.progress);
}
});
FlDownloader.download(widget.url, fileName: 'adguard-home-manager_v${widget.version}.apk');
} catch (_) {
}
}
@override
void initState() {
super.initState();
download();
}
@override
void dispose() {
super.dispose();
progressStream.cancel();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("${AppLocalizations.of(context)!.downloadingUpdate}..."),
titlePadding: const EdgeInsets.only(
left: 24, right: 24, top: 24, bottom: 10,
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearPercentIndicator(
animation: true,
lineHeight: 4,
animationDuration: 500,
curve: Curves.easeOut,
percent: progress/100,
animateFromLastPercent: true,
barRadius: const Radius.circular(5),
progressColor: Theme.of(context).primaryColor,
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: Text("$progress% ${AppLocalizations.of(context)!.completed}"),
)
],
)
],
),
);
}
}

View file

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/models/github_release.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class UpdateModal extends StatefulWidget {
final GitHubRelease gitHubRelease;
final void Function(String, String) onDownload;
const UpdateModal({
Key? key,
required this.gitHubRelease,
required this.onDownload,
}) : super(key: key);
@override
State<UpdateModal> createState() => _UpdateModalState();
}
class _UpdateModalState extends State<UpdateModal> {
bool doNotRemember = false;
String getDownloadLink() {
return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('apk')).browserDownloadUrl;
}
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
return AlertDialog(
scrollable: true,
title: Column(
children: [
const Icon(
Icons.system_update_rounded,
size: 26,
),
const SizedBox(height: 20),
Text(
AppLocalizations.of(context)!.updateAvailable,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 26
),
)
],
),
content: Column(
children: [
const SizedBox(height: 10),
Text("${AppLocalizations.of(context)!.installedVersion}: ${appConfigProvider.getAppInfo!.version}"),
const SizedBox(height: 10),
Text("${AppLocalizations.of(context)!.newVersion}: ${widget.gitHubRelease.tagName}"),
const SizedBox(height: 10),
Text("${AppLocalizations.of(context)!.source}: GitHub"),
const SizedBox(height: 20),
GestureDetector(
onTap: () => setState(() => doNotRemember = !doNotRemember),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Checkbox(
value: doNotRemember,
onChanged: (value) => setState(() => doNotRemember = value!),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)
),
),
const SizedBox(width: 10),
Flexible(child: Text(AppLocalizations.of(context)!.doNotRememberAgainUpdate))
],
),
)
],
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
widget.onDownload(getDownloadLink(), widget.gitHubRelease.tagName);
},
child: Text(AppLocalizations.of(context)!.download)
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.close)
),
],
)
],
);
}
}

View file

@ -197,6 +197,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.55.2"
fl_downloader:
dependency: "direct main"
description:
name: fl_downloader
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
flutter:
dependency: "direct main"
description: flutter
@ -287,6 +294,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
install_plugin_v2:
dependency: "direct main"
description:
name: install_plugin_v2
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
intl:
dependency: "direct main"
description:
@ -413,6 +427,41 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.2"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "10.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
url: "https://pub.dartlang.org"
source: hosted
version: "10.2.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.7"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.9.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
petitparser:
dependency: transitive
description:
@ -467,6 +516,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
store_checker:
dependency: "direct main"
description:
name: store_checker
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
stream_channel:
dependency: transitive
description:

View file

@ -49,6 +49,10 @@ dependencies:
flutter_svg: ^1.1.5
bottom_sheet: ^3.1.2
percent_indicator: ^4.2.2
store_checker: ^1.1.0
fl_downloader: ^1.0.3
install_plugin_v2: ^1.0.0
permission_handler: ^10.2.0
dev_dependencies:
flutter_test: