Skip to content

Commit 33530f1

Browse files
Support downloading and installing apk
1 parent 3607c74 commit 33530f1

File tree

12 files changed

+300
-18
lines changed

12 files changed

+300
-18
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
<uses-permission android:name="android.permission.VIBRATE" />
1212
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
13+
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
14+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
1315
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
1416
<uses-permission android:name="android.permission.INTERNET" />
1517
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

lib/Screens/Setting/general_setting_screen.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import '../../Providers/global_provider.dart';
1212
import '../../Providers/provider_manager.dart';
1313
import '../../Utils/hive_util.dart';
1414
import '../../Utils/locale_util.dart';
15-
import '../../Utils/uri_util.dart';
1615
import '../../Utils/utils.dart';
1716
import '../../Widgets/BottomSheet/bottom_sheet_builder.dart';
1817
import '../../Widgets/BottomSheet/list_bottom_sheet.dart';
@@ -102,11 +101,18 @@ class _GeneralSettingScreenState extends State<GeneralSettingScreen>
102101
title: "发现新版本$latestVersion",
103102
message:
104103
"是否立即更新?${Utils.isNotEmpty(latestReleaseItem!.body) ? "更新日志如下:\n${latestReleaseItem!.body}" : ""}",
105-
confirmButtonText: "前往更新",
104+
confirmButtonText: "立即下载",
106105
cancelButtonText: "暂不更新",
107106
onTapConfirm: () {
108107
Navigator.pop(context);
109-
UriUtil.openExternal(latestReleaseItem!.htmlUrl);
108+
Utils.downloadAndUpdate(
109+
context,
110+
latestReleaseItem!.assets.isNotEmpty
111+
? latestReleaseItem!.assets[0].browserDownloadUrl
112+
: "",
113+
latestReleaseItem!.htmlUrl,
114+
version: latestVersion,
115+
);
110116
},
111117
onTapCancel: () {
112118
Navigator.pop(context);

lib/Screens/Setting/update_log_screen.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,16 @@ class _UpdateLogScreenState extends State<UpdateLogScreen>
137137
color: Theme.of(context).textTheme.labelMedium?.color,
138138
),
139139
onTap: () {
140-
UriUtil.launchUrlUri(context,item.assets[0].browserDownloadUrl);
140+
Utils.downloadAndUpdate(
141+
context,
142+
item.assets.isNotEmpty
143+
? item.assets[0].browserDownloadUrl
144+
: "",
145+
item.htmlUrl,
146+
version:
147+
item.tagName.replaceAll(RegExp(r'[a-zA-Z]'), ''),
148+
isUpdate: false,
149+
);
141150
},
142151
),
143152
],

lib/Screens/main_screen.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,18 @@ class MainScreenState extends State<MainScreen>
9292
title: "发现新版本$latestVersion",
9393
message:
9494
"是否立即更新?${Utils.isNotEmpty(latestReleaseItem!.body) ? "更新日志如下:\n${latestReleaseItem!.body}" : ""}",
95-
confirmButtonText: "前往更新",
95+
confirmButtonText: "立即下载",
9696
cancelButtonText: "暂不更新",
9797
onTapConfirm: () {
9898
Navigator.pop(context);
99-
UriUtil.openExternal(latestReleaseItem!.htmlUrl);
99+
Utils.downloadAndUpdate(
100+
context,
101+
latestReleaseItem!.assets.isNotEmpty
102+
? latestReleaseItem!.assets[0].browserDownloadUrl
103+
: "",
104+
latestReleaseItem!.htmlUrl,
105+
version: latestVersion,
106+
);
100107
},
101108
onTapCancel: () {
102109
Navigator.pop(context);
@@ -183,7 +190,8 @@ class MainScreenState extends State<MainScreen>
183190
} else if (index == _bottomBarSelectedIndex &&
184191
_pageList[index] is DynamicScreen &&
185192
_keyList[index].currentState != null) {
186-
(_keyList[index].currentState as DynamicScreenState).scrollToTopAndRefresh();
193+
(_keyList[index].currentState as DynamicScreenState)
194+
.scrollToTopAndRefresh();
187195
}
188196

189197
_pageController.jumpToPage(index);

lib/Utils/notification_util.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
2+
import 'package:install_plugin/install_plugin.dart';
3+
import 'package:loftify/Utils/utils.dart';
4+
5+
class NotificationUtil {
6+
static final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
7+
FlutterLocalNotificationsPlugin();
8+
9+
static init() async {
10+
var android = const AndroidInitializationSettings("@mipmap/ic_launcher");
11+
await flutterLocalNotificationsPlugin.initialize(
12+
InitializationSettings(android: android),
13+
onDidReceiveNotificationResponse: (respose) async {
14+
if (respose.id == 1 && Utils.isNotEmpty(respose.payload)) {
15+
await InstallPlugin.install(respose.payload!);
16+
}
17+
},
18+
);
19+
flutterLocalNotificationsPlugin
20+
.resolvePlatformSpecificImplementation<
21+
AndroidFlutterLocalNotificationsPlugin>()
22+
?.requestNotificationsPermission();
23+
}
24+
25+
static Future<void> closeNotification(int id) async {
26+
return flutterLocalNotificationsPlugin.cancel(id);
27+
}
28+
29+
static Future<void> sendProgressNotification(
30+
int id,
31+
int progress, {
32+
String? title,
33+
String? payload,
34+
}) async {
35+
AndroidNotificationDetails androidPlatformChannelSpecifics =
36+
AndroidNotificationDetails(
37+
'progress channel',
38+
'progress channel',
39+
channelDescription: 'Notification channel for showing progress',
40+
importance: Importance.low,
41+
priority: Priority.low,
42+
showProgress: true,
43+
maxProgress: 100,
44+
progress: progress,
45+
);
46+
NotificationDetails platformChannelSpecifics =
47+
NotificationDetails(android: androidPlatformChannelSpecifics);
48+
await flutterLocalNotificationsPlugin.show(
49+
id,
50+
title,
51+
'$progress%',
52+
platformChannelSpecifics,
53+
payload: payload,
54+
);
55+
}
56+
57+
static Future<void> sendInfoNotification(
58+
int id,
59+
String title,
60+
String body, {
61+
String? payload,
62+
}) async {
63+
const AndroidNotificationDetails androidPlatformChannelSpecifics =
64+
AndroidNotificationDetails(
65+
'download complete channel',
66+
'download complete channel',
67+
channelDescription: 'Notification channel for showing download complete',
68+
importance: Importance.high,
69+
priority: Priority.high,
70+
);
71+
const NotificationDetails platformChannelSpecifics =
72+
NotificationDetails(android: androidPlatformChannelSpecifics);
73+
await flutterLocalNotificationsPlugin.show(
74+
id,
75+
title,
76+
body,
77+
platformChannelSpecifics,
78+
payload: payload,
79+
);
80+
}
81+
}
82+
83+
var notification = NotificationUtil();

lib/Utils/utils.dart

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
1313
import 'package:html/parser.dart';
1414
import 'package:http/http.dart' as http;
1515
import 'package:image_gallery_saver/image_gallery_saver.dart';
16+
import 'package:install_plugin/install_plugin.dart';
1617
import 'package:intl/intl.dart';
1718
import 'package:loftify/Models/enums.dart';
1819
import 'package:loftify/Utils/hive_util.dart';
20+
import 'package:loftify/Utils/iprint.dart';
21+
import 'package:loftify/Utils/notification_util.dart';
22+
import 'package:loftify/Utils/uri_util.dart';
1923
import 'package:loftify/Widgets/BottomSheet/slide_captcha_bottom_sheet.dart';
24+
import 'package:loftify/Widgets/Dialog/custom_dialog.dart';
2025
import 'package:loftify/Widgets/Item/item_builder.dart';
2126
import 'package:palette_generator/palette_generator.dart';
2227
import 'package:path_provider/path_provider.dart';
28+
import 'package:permission_handler/permission_handler.dart';
2329
import 'package:restart_app/restart_app.dart';
2430
import 'package:share_plus/share_plus.dart';
2531

@@ -105,7 +111,6 @@ class Utils {
105111
Utils.removeImageParam(element) == Utils.removeImageParam(image));
106112
}
107113

108-
//从imageUrl中提取出文件名
109114
static String extractFileNameFromUrl(String imageUrl) {
110115
return Uri.parse(imageUrl).pathSegments.last;
111116
}
@@ -594,4 +599,82 @@ class Utils {
594599
},
595600
);
596601
}
602+
603+
static Future<void> downloadAndUpdate(
604+
BuildContext context,
605+
String apkUrl,
606+
String htmlUrl, {
607+
String? version,
608+
bool isUpdate = true,
609+
Function(double)? onReceiveProgress,
610+
}) async {
611+
await Permission.storage.onDeniedCallback(() {
612+
IToast.showTop(context, text: "请授予文件存储权限");
613+
}).onGrantedCallback(() async {
614+
if (Utils.isNotEmpty(apkUrl)) {
615+
double progressValue = 0.0;
616+
var appDocDir = await getTemporaryDirectory();
617+
String savePath =
618+
"${appDocDir.path}/${Utils.extractFileNameFromUrl(apkUrl)}";
619+
try {
620+
await Dio().download(
621+
apkUrl,
622+
savePath,
623+
onReceiveProgress: (count, total) {
624+
final value = count / total;
625+
if (progressValue != value) {
626+
if (progressValue < 1.0) {
627+
progressValue = count / total;
628+
} else {
629+
progressValue = 0.0;
630+
}
631+
NotificationUtil.sendProgressNotification(
632+
0,
633+
(progressValue * 100).toInt(),
634+
title: isUpdate
635+
? '正在下载新版本安装包...'
636+
: '正在下载版本${version ?? ""}的安装包...',
637+
payload: version ?? "",
638+
);
639+
onReceiveProgress?.call(progressValue);
640+
}
641+
},
642+
).then((response) async {
643+
if (response.statusCode == 200) {
644+
NotificationUtil.closeNotification(0);
645+
NotificationUtil.sendInfoNotification(
646+
1,
647+
"下载完成",
648+
isUpdate
649+
? "新版本安装包已经下载完成,点击立即安装"
650+
: "版本${version ?? ""}的安装包已经下载完成,点击立即安装",
651+
payload: savePath,
652+
);
653+
} else {
654+
UriUtil.openExternal(htmlUrl);
655+
}
656+
});
657+
} catch (e) {
658+
IPrint.debug(e);
659+
NotificationUtil.closeNotification(0);
660+
NotificationUtil.sendInfoNotification(
661+
2,
662+
"下载失败,请重试",
663+
"新版本安装包下载失败,请重试",
664+
);
665+
}
666+
} else {
667+
UriUtil.openExternal(htmlUrl);
668+
}
669+
}).onPermanentlyDeniedCallback(() {
670+
IToast.showTop(context, text: "已拒绝文件存储权限,将跳转到浏览器下载");
671+
UriUtil.openExternal(apkUrl);
672+
}).onRestrictedCallback(() {
673+
IToast.showTop(context, text: "请授予文件存储权限");
674+
}).onLimitedCallback(() {
675+
IToast.showTop(context, text: "请授予文件存储权限");
676+
}).onProvisionalCallback(() {
677+
IToast.showTop(context, text: "请授予文件存储权限");
678+
}).request();
679+
}
597680
}

lib/main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
55
import 'package:flutter_displaymode/flutter_displaymode.dart';
66
import 'package:flutter_localizations/flutter_localizations.dart';
77
import 'package:flutter_native_splash/flutter_native_splash.dart';
8+
import 'package:hive_flutter/adapters.dart';
89
import 'package:loftify/Models/recommend_response.dart';
910
import 'package:loftify/Providers/global_provider.dart';
1011
import 'package:loftify/Screens/Info/favorite_folder_list_screen.dart';
@@ -41,6 +42,7 @@ import 'Screens/Navigation/dynamic_screen.dart';
4142
import 'Screens/Navigation/home_screen.dart';
4243
import 'Screens/Setting/about_setting_screen.dart';
4344
import 'Screens/main_screen.dart';
45+
import 'Utils/notification_util.dart';
4446
import 'generated/l10n.dart';
4547

4648
Future<void> main() async {
@@ -49,6 +51,7 @@ Future<void> main() async {
4951
PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 1024 * 2;
5052
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
5153
await ProviderManager.init();
54+
NotificationUtil.init();
5255
await RequestHeaderUtil.initAndroidInfo();
5356
SystemChrome.setPreferredOrientations(
5457
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import app_links
99
import audio_session
1010
import device_info_plus
1111
import flutter_inappwebview_macos
12+
import flutter_local_notifications
1213
import just_audio
1314
import package_info_plus
1415
import path_provider_foundation
@@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
2324
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
2425
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
2526
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
27+
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
2628
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
2729
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
2830
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

0 commit comments

Comments
 (0)