Skip to content

Commit

Permalink
Support downloading and installing apk
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert-Stackflow committed Jul 4, 2024
1 parent 3607c74 commit 33530f1
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 18 deletions.
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
Expand Down
12 changes: 9 additions & 3 deletions lib/Screens/Setting/general_setting_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import '../../Providers/global_provider.dart';
import '../../Providers/provider_manager.dart';
import '../../Utils/hive_util.dart';
import '../../Utils/locale_util.dart';
import '../../Utils/uri_util.dart';
import '../../Utils/utils.dart';
import '../../Widgets/BottomSheet/bottom_sheet_builder.dart';
import '../../Widgets/BottomSheet/list_bottom_sheet.dart';
Expand Down Expand Up @@ -102,11 +101,18 @@ class _GeneralSettingScreenState extends State<GeneralSettingScreen>
title: "发现新版本$latestVersion",
message:
"是否立即更新?${Utils.isNotEmpty(latestReleaseItem!.body) ? "更新日志如下:\n${latestReleaseItem!.body}" : ""}",
confirmButtonText: "前往更新",
confirmButtonText: "立即下载",
cancelButtonText: "暂不更新",
onTapConfirm: () {
Navigator.pop(context);
UriUtil.openExternal(latestReleaseItem!.htmlUrl);
Utils.downloadAndUpdate(
context,
latestReleaseItem!.assets.isNotEmpty
? latestReleaseItem!.assets[0].browserDownloadUrl
: "",
latestReleaseItem!.htmlUrl,
version: latestVersion,
);
},
onTapCancel: () {
Navigator.pop(context);
Expand Down
11 changes: 10 additions & 1 deletion lib/Screens/Setting/update_log_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,16 @@ class _UpdateLogScreenState extends State<UpdateLogScreen>
color: Theme.of(context).textTheme.labelMedium?.color,
),
onTap: () {
UriUtil.launchUrlUri(context,item.assets[0].browserDownloadUrl);
Utils.downloadAndUpdate(
context,
item.assets.isNotEmpty
? item.assets[0].browserDownloadUrl
: "",
item.htmlUrl,
version:
item.tagName.replaceAll(RegExp(r'[a-zA-Z]'), ''),
isUpdate: false,
);
},
),
],
Expand Down
14 changes: 11 additions & 3 deletions lib/Screens/main_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,18 @@ class MainScreenState extends State<MainScreen>
title: "发现新版本$latestVersion",
message:
"是否立即更新?${Utils.isNotEmpty(latestReleaseItem!.body) ? "更新日志如下:\n${latestReleaseItem!.body}" : ""}",
confirmButtonText: "前往更新",
confirmButtonText: "立即下载",
cancelButtonText: "暂不更新",
onTapConfirm: () {
Navigator.pop(context);
UriUtil.openExternal(latestReleaseItem!.htmlUrl);
Utils.downloadAndUpdate(
context,
latestReleaseItem!.assets.isNotEmpty
? latestReleaseItem!.assets[0].browserDownloadUrl
: "",
latestReleaseItem!.htmlUrl,
version: latestVersion,
);
},
onTapCancel: () {
Navigator.pop(context);
Expand Down Expand Up @@ -183,7 +190,8 @@ class MainScreenState extends State<MainScreen>
} else if (index == _bottomBarSelectedIndex &&
_pageList[index] is DynamicScreen &&
_keyList[index].currentState != null) {
(_keyList[index].currentState as DynamicScreenState).scrollToTopAndRefresh();
(_keyList[index].currentState as DynamicScreenState)
.scrollToTopAndRefresh();
}

_pageController.jumpToPage(index);
Expand Down
83 changes: 83 additions & 0 deletions lib/Utils/notification_util.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:loftify/Utils/utils.dart';

class NotificationUtil {
static final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();

static init() async {
var android = const AndroidInitializationSettings("@mipmap/ic_launcher");
await flutterLocalNotificationsPlugin.initialize(
InitializationSettings(android: android),
onDidReceiveNotificationResponse: (respose) async {
if (respose.id == 1 && Utils.isNotEmpty(respose.payload)) {
await InstallPlugin.install(respose.payload!);
}
},
);
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
}

static Future<void> closeNotification(int id) async {
return flutterLocalNotificationsPlugin.cancel(id);
}

static Future<void> sendProgressNotification(
int id,
int progress, {
String? title,
String? payload,
}) async {
AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'progress channel',
'progress channel',
channelDescription: 'Notification channel for showing progress',
importance: Importance.low,
priority: Priority.low,
showProgress: true,
maxProgress: 100,
progress: progress,
);
NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
id,
title,
'$progress%',
platformChannelSpecifics,
payload: payload,
);
}

static Future<void> sendInfoNotification(
int id,
String title,
String body, {
String? payload,
}) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'download complete channel',
'download complete channel',
channelDescription: 'Notification channel for showing download complete',
importance: Importance.high,
priority: Priority.high,
);
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
id,
title,
body,
platformChannelSpecifics,
payload: payload,
);
}
}

var notification = NotificationUtil();
85 changes: 84 additions & 1 deletion lib/Utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart' as http;
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:intl/intl.dart';
import 'package:loftify/Models/enums.dart';
import 'package:loftify/Utils/hive_util.dart';
import 'package:loftify/Utils/iprint.dart';
import 'package:loftify/Utils/notification_util.dart';
import 'package:loftify/Utils/uri_util.dart';
import 'package:loftify/Widgets/BottomSheet/slide_captcha_bottom_sheet.dart';
import 'package:loftify/Widgets/Dialog/custom_dialog.dart';
import 'package:loftify/Widgets/Item/item_builder.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:restart_app/restart_app.dart';
import 'package:share_plus/share_plus.dart';

Expand Down Expand Up @@ -105,7 +111,6 @@ class Utils {
Utils.removeImageParam(element) == Utils.removeImageParam(image));
}

//从imageUrl中提取出文件名
static String extractFileNameFromUrl(String imageUrl) {
return Uri.parse(imageUrl).pathSegments.last;
}
Expand Down Expand Up @@ -594,4 +599,82 @@ class Utils {
},
);
}

static Future<void> downloadAndUpdate(
BuildContext context,
String apkUrl,
String htmlUrl, {
String? version,
bool isUpdate = true,
Function(double)? onReceiveProgress,
}) async {
await Permission.storage.onDeniedCallback(() {
IToast.showTop(context, text: "请授予文件存储权限");
}).onGrantedCallback(() async {
if (Utils.isNotEmpty(apkUrl)) {
double progressValue = 0.0;
var appDocDir = await getTemporaryDirectory();
String savePath =
"${appDocDir.path}/${Utils.extractFileNameFromUrl(apkUrl)}";
try {
await Dio().download(
apkUrl,
savePath,
onReceiveProgress: (count, total) {
final value = count / total;
if (progressValue != value) {
if (progressValue < 1.0) {
progressValue = count / total;
} else {
progressValue = 0.0;
}
NotificationUtil.sendProgressNotification(
0,
(progressValue * 100).toInt(),
title: isUpdate
? '正在下载新版本安装包...'
: '正在下载版本${version ?? ""}的安装包...',
payload: version ?? "",
);
onReceiveProgress?.call(progressValue);
}
},
).then((response) async {
if (response.statusCode == 200) {
NotificationUtil.closeNotification(0);
NotificationUtil.sendInfoNotification(
1,
"下载完成",
isUpdate
? "新版本安装包已经下载完成,点击立即安装"
: "版本${version ?? ""}的安装包已经下载完成,点击立即安装",
payload: savePath,
);
} else {
UriUtil.openExternal(htmlUrl);
}
});
} catch (e) {
IPrint.debug(e);
NotificationUtil.closeNotification(0);
NotificationUtil.sendInfoNotification(
2,
"下载失败,请重试",
"新版本安装包下载失败,请重试",
);
}
} else {
UriUtil.openExternal(htmlUrl);
}
}).onPermanentlyDeniedCallback(() {
IToast.showTop(context, text: "已拒绝文件存储权限,将跳转到浏览器下载");
UriUtil.openExternal(apkUrl);
}).onRestrictedCallback(() {
IToast.showTop(context, text: "请授予文件存储权限");
}).onLimitedCallback(() {
IToast.showTop(context, text: "请授予文件存储权限");
}).onProvisionalCallback(() {
IToast.showTop(context, text: "请授予文件存储权限");
}).request();
}
}
3 changes: 3 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:loftify/Models/recommend_response.dart';
import 'package:loftify/Providers/global_provider.dart';
import 'package:loftify/Screens/Info/favorite_folder_list_screen.dart';
Expand Down Expand Up @@ -41,6 +42,7 @@ import 'Screens/Navigation/dynamic_screen.dart';
import 'Screens/Navigation/home_screen.dart';
import 'Screens/Setting/about_setting_screen.dart';
import 'Screens/main_screen.dart';
import 'Utils/notification_util.dart';
import 'generated/l10n.dart';

Future<void> main() async {
Expand All @@ -49,6 +51,7 @@ Future<void> main() async {
PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 1024 * 2;
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await ProviderManager.init();
NotificationUtil.init();
await RequestHeaderUtil.initAndroidInfo();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
Expand Down
2 changes: 2 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import app_links
import audio_session
import device_info_plus
import flutter_inappwebview_macos
import flutter_local_notifications
import just_audio
import package_info_plus
import path_provider_foundation
Expand All @@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
Expand Down
Loading

0 comments on commit 33530f1

Please sign in to comment.