import 'dart:async'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart' as bd; import 'package:flutter/foundation.dart'; import 'package:external_path/external_path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import '../core/constants/storage_keys.dart'; import '../data/models/download_task_model.dart'; import 'file_service.dart'; import 'storage_service.dart'; import '../core/utils/app_logger.dart'; /// 下载服务 - 单例模式 /// 所有平台统一使用 background_downloader class DownloadService { static final DownloadService _instance = DownloadService._internal(); factory DownloadService() => _instance; DownloadService._internal(); // 统一映射:外部下载器 task ID → 内部 task ID final Map _externalTaskIdToInternalId = {}; final Map _internalIdToExternalTaskId = {}; // 存储 background_downloader 的 DownloadTask 对象,用于暂停/恢复/取消 final Map _bdTasks = {}; // 回调处理器 static Function(String taskId, DownloadStatus status, int progress)? _callbackHandler; final FileService _fileService = FileService(); final Map> _progressControllers = {}; bool _isInitialized = false; /// 设置回调处理器 static void setCallbackHandler( Function(String taskId, DownloadStatus status, int progress) handler, ) { _callbackHandler = handler; } /// 获取下载任务进度流 Stream getProgressStream(String taskId) { if (!_progressControllers.containsKey(taskId)) { _progressControllers[taskId] = StreamController.broadcast(); } return _progressControllers[taskId]!.stream; } /// 获取下载目录 Future getDownloadDirectory() async { if (defaultTargetPlatform == TargetPlatform.android) { if (await Permission.notification.isDenied) { await Permission.notification.request(); } final status = await Permission.manageExternalStorage.request(); if (status.isPermanentlyDenied) { throw Exception('存储权限被永久拒绝,请在设置中开启'); } if (!status.isGranted) { throw Exception('存储权限被拒绝'); } final downloadPath = await ExternalPath.getExternalStoragePublicDirectory( ExternalPath.DIRECTORY_DOWNLOAD, ); final directory = Directory(downloadPath); if (!await directory.exists()) { await directory.create(recursive: true); } return directory; } else if (defaultTargetPlatform == TargetPlatform.iOS) { final appDocDir = await getApplicationDocumentsDirectory(); final directory = Directory('${appDocDir.path}/Downloads'); if (!await directory.exists()) { await directory.create(recursive: true); } return directory; } else { // Windows/Linux/macOS - 使用系统下载目录 final downloadsDir = await getDownloadsDirectory(); if (downloadsDir != null) { return downloadsDir; } // 回退方案 if (Platform.isWindows) { final userProfile = Platform.environment['USERPROFILE'] ?? ''; final dir = Directory('$userProfile\\Downloads'); if (!await dir.exists()) { await dir.create(recursive: true); } return dir; } final home = Platform.environment['HOME'] ?? ''; final dir = Directory('$home/Downloads'); if (!await dir.exists()) { await dir.create(recursive: true); } return dir; } } /// 读取 WiFi-only 下载设置 Future isWifiOnlyEnabled() async { return await StorageService.instance.getBool( StorageKeys.downloadWifiOnly, ) ?? false; } /// 读取重试次数设置 Future getRetries() async { return await StorageService.instance.getInt(StorageKeys.downloadRetries) ?? 3; } /// 初始化下载器 Future initialize({ Function(String taskId, DownloadStatus status, int progress)? callbackHandler, }) async { if (callbackHandler != null) { setCallbackHandler(callbackHandler); AppLogger.d('回调处理器已更新'); } if (_isInitialized) { AppLogger.d('DownloadService 已经初始化'); return; } // 配置通知(Android 前台服务需要通知栏显示) if (Platform.isAndroid) { bd.FileDownloader().configureNotification( running: const bd.TaskNotification( '正在下载', '文件: {filename} - {progress}', ), complete: const bd.TaskNotification('下载完成', '文件: {filename} 已保存'), error: const bd.TaskNotification('下载失败', '文件: {filename} 下载出错'), paused: const bd.TaskNotification('已暂停', '文件: {filename} 已暂停'), progressBar: true, tapOpensFile: true, ); AppLogger.d('background_downloader 通知已配置'); } bd.FileDownloader().registerCallbacks( taskStatusCallback: _handleBdStatusUpdate, taskProgressCallback: _handleBdProgressUpdate, ); // 启动任务追踪和恢复 await bd.FileDownloader().start( doTrackTasks: true, markDownloadedComplete: true, doRescheduleKilledTasks: true, ); _isInitialized = true; AppLogger.d('DownloadService 初始化完成 (background_downloader)'); } /// background_downloader 状态回调 void _handleBdStatusUpdate(bd.TaskStatusUpdate update) { final internalId = _externalTaskIdToInternalId[update.task.taskId]; // 如果映射不存在,尝试从 metaData 恢复 if (internalId == null && update.task.metaData.isNotEmpty) { final metaInternalId = update.task.metaData; _externalTaskIdToInternalId[update.task.taskId] = metaInternalId; _internalIdToExternalTaskId[metaInternalId] = update.task.taskId; _bdTasks[metaInternalId] = update.task as bd.DownloadTask; AppLogger.d( '从 metaData 恢复映射: bdTaskId=${update.task.taskId}, internalId=$metaInternalId', ); } final resolvedInternalId = _externalTaskIdToInternalId[update.task.taskId]; if (resolvedInternalId == null) { AppLogger.d( 'background_downloader 状态回调: 未找到内部任务ID, taskId=${update.task.taskId}', ); return; } DownloadStatus status; switch (update.status) { case bd.TaskStatus.enqueued: status = DownloadStatus.waiting; case bd.TaskStatus.running: status = DownloadStatus.downloading; case bd.TaskStatus.complete: status = DownloadStatus.completed; case bd.TaskStatus.notFound: case bd.TaskStatus.failed: status = DownloadStatus.failed; case bd.TaskStatus.canceled: status = DownloadStatus.cancelled; case bd.TaskStatus.paused: status = DownloadStatus.paused; case bd.TaskStatus.waitingToRetry: status = DownloadStatus.waiting; } AppLogger.d( 'background_downloader 状态更新: taskId=${update.task.taskId}, internalId=$resolvedInternalId, status=$status', ); final progress = status == DownloadStatus.completed ? 100 : 0; _callbackHandler?.call(resolvedInternalId, status, progress); } /// background_downloader 进度回调 void _handleBdProgressUpdate(bd.TaskProgressUpdate update) { final internalId = _externalTaskIdToInternalId[update.task.taskId]; if (internalId == null) return; final progress = (update.progress * 100).toInt().clamp(0, 100); _callbackHandler?.call(internalId, DownloadStatus.downloading, progress); } /// 开始下载(新任务) Future startDownload(DownloadTaskModel task) async { try { if (!_isInitialized) { await initialize(); } // 获取下载 URL String url = task.downloadUrl ?? ''; if (url.isEmpty) { final response = await _fileService.getDownloadUrls( uris: [task.fileUri], download: true, ); final urls = response['urls'] as List? ?? []; if (urls.isEmpty) { throw Exception('无法获取下载链接'); } final urlData = urls[0] as Map; url = urlData['url'] as String; } // 确保保存目录存在 final file = File(task.savePath); final dir = file.parent; if (!await dir.exists()) { await dir.create(recursive: true); } // 新任务:如果完整文件已存在,删除它 if (await file.exists() && task.downloadedBytes == 0) { await file.delete(); } return _startBdDownload(task, url, dir); } catch (e) { AppLogger.d('下载失败: $e'); rethrow; } } /// 使用 background_downloader 开始下载 Future _startBdDownload( DownloadTaskModel task, String url, Directory dir, ) async { final wifiOnly = await isWifiOnlyEnabled(); final retries = await getRetries(); final bdTask = bd.DownloadTask( url: url, filename: task.fileName, directory: dir.path, baseDirectory: bd.BaseDirectory.root, updates: bd.Updates.statusAndProgress, allowPause: true, retries: retries, requiresWiFi: wifiOnly, metaData: task.id, ); final success = await bd.FileDownloader().enqueue(bdTask); if (!success) { throw Exception('创建下载任务失败'); } // 保存映射关系 _externalTaskIdToInternalId[bdTask.taskId] = task.id; _internalIdToExternalTaskId[task.id] = bdTask.taskId; _bdTasks[task.id] = bdTask; // 保存 backgroundTaskId 到任务模型(持久化,用于重启后恢复映射) task.backgroundTaskId = bdTask.taskId; AppLogger.d( 'background_downloader 任务已添加: taskId=${bdTask.taskId}, internalId=${task.id}, requiresWiFi=$wifiOnly, retries=$retries', ); return bdTask.taskId; } /// 恢复下载(用于重启后恢复暂停的任务) Future resumeDownloadAfterRestart(DownloadTaskModel task) async { try { if (!_isInitialized) { await initialize(); } // 获取下载 URL String url = task.downloadUrl ?? ''; if (url.isEmpty) { final response = await _fileService.getDownloadUrls( uris: [task.fileUri], download: true, ); final urls = response['urls'] as List? ?? []; if (urls.isEmpty) { throw Exception('无法获取下载链接'); } final urlData = urls[0] as Map; url = urlData['url'] as String; } final file = File(task.savePath); final dir = file.parent; if (!await dir.exists()) { await dir.create(recursive: true); } // 恢复任务:不删除部分文件,使用 resume 方式重建 bdTask final wifiOnly = await isWifiOnlyEnabled(); final retries = await getRetries(); final bdTask = bd.DownloadTask( url: url, filename: task.fileName, directory: dir.path, baseDirectory: bd.BaseDirectory.root, updates: bd.Updates.statusAndProgress, allowPause: true, retries: retries, requiresWiFi: wifiOnly, metaData: task.id, ); // 恢复映射关系 _externalTaskIdToInternalId[bdTask.taskId] = task.id; _internalIdToExternalTaskId[task.id] = bdTask.taskId; _bdTasks[task.id] = bdTask; task.backgroundTaskId = bdTask.taskId; // 如果有已下载的部分,尝试 resume;否则 enqueue final partialFile = File('${dir.path}/${task.fileName}.part'); if (task.downloadedBytes > 0 && await partialFile.exists()) { AppLogger.d( '断点续传: ${task.fileName}, 已下载 ${task.downloadedBytes} bytes', ); await bd.FileDownloader().resume(bdTask); } else { AppLogger.d('重新下载: ${task.fileName}'); final success = await bd.FileDownloader().enqueue(bdTask); if (!success) { throw Exception('创建下载任务失败'); } } return bdTask.taskId; } catch (e) { AppLogger.d('恢复下载失败: $e'); rethrow; } } /// 暂停下载 Future pauseDownload(String taskId) async { final bdTask = _bdTasks[taskId]; if (bdTask != null) { await bd.FileDownloader().pause(bdTask); } } /// 恢复下载 Future resumeDownload(String taskId) async { if (!_isInitialized) { await initialize(); } final bdTask = _bdTasks[taskId]; if (bdTask != null) { await bd.FileDownloader().resume(bdTask); } } /// 取消下载 Future cancelDownload(String taskId) async { final bdTask = _bdTasks[taskId]; if (bdTask != null) { await bd.FileDownloader().cancel(bdTask); } } /// 删除下载任务 void disposeTask(String taskId) { final externalId = _internalIdToExternalTaskId[taskId]; if (externalId != null) { _externalTaskIdToInternalId.remove(externalId); } _internalIdToExternalTaskId.remove(taskId); _bdTasks.remove(taskId); // 关闭进度流 final controller = _progressControllers[taskId]; if (controller != null) { controller.close(); _progressControllers.remove(taskId); } } /// 删除已下载的文件 Future deleteDownloadedFile(String savePath) async { try { final file = File(savePath); if (await file.exists()) { await file.delete(); } // 同时删除部分文件 final partialFile = File('$savePath.part'); if (await partialFile.exists()) { await partialFile.delete(); } } catch (e) { AppLogger.d('删除文件失败: $e'); } } /// 获取可读的文件大小 static String getReadableFileSize(int bytes) { if (bytes < 1024) { return '$bytes B'; } else if (bytes < 1024 * 1024) { return '${(bytes / 1024).toStringAsFixed(1)} KB'; } else if (bytes < 1024 * 1024 * 1024) { return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } else { return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; } } /// 清理所有资源 void dispose() { _externalTaskIdToInternalId.clear(); _internalIdToExternalTaskId.clear(); _bdTasks.clear(); // 关闭所有流 for (final controller in _progressControllers.values) { if (!controller.isClosed) { controller.close(); } } _progressControllers.clear(); } }