import 'dart:async'; import 'dart:developer' as developer; import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import '../local_storage/key_value_storage.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../type/episode_task.dart'; import '../type/episodebrief.dart'; void downloadCallback(String id, DownloadTaskStatus status, int progress) { developer.log('Homepage callback task in $id status ($status) $progress'); final send = IsolateNameServer.lookupPortByName('downloader_send_port'); send.send([id, status, progress]); } void autoDownloadCallback(String id, DownloadTaskStatus status, int progress) { developer .log('Autodownload callback task in $id status ($status) $progress'); final send = IsolateNameServer.lookupPortByName('auto_downloader_send_port'); send.send([id, status, progress]); } //For background auto downlaod class AutoDownloader { final DBHelper _dbHelper = DBHelper(); final List _episodeTasks = []; final Completer _completer = Completer(); AutoDownloader() { FlutterDownloader.registerCallback(autoDownloadCallback); } bindBackgroundIsolate() { var _port = ReceivePort(); var isSuccess = IsolateNameServer.registerPortWithName( _port.sendPort, 'auto_downloader_send_port'); if (!isSuccess) { IsolateNameServer.removePortNameMapping('auto_downloader_send_port'); bindBackgroundIsolate(); return; } _port.listen((dynamic data) { String id = data[0]; DownloadTaskStatus status = data[1]; int progress = data[2]; for (var episodeTask in _episodeTasks) { if (episodeTask.taskId == id) { episodeTask.status = status; episodeTask.progress = progress; if (status == DownloadTaskStatus.complete) { _saveMediaId(episodeTask); } else if (status == DownloadTaskStatus.failed) { _episodeTasks.removeWhere((element) => element.episode.enclosureUrl == episodeTask.episode.enclosureUrl); if (_episodeTasks.length == 0) _unbindBackgroundIsolate(); } } } }); } void _unbindBackgroundIsolate() { IsolateNameServer.removePortNameMapping('auto_downloader_send_port'); _completer?.complete(); } Future _getDownloadDirectory() async { final storage = KeyValueStorage(downloadPositionKey); final index = await storage.getInt(); final externalDirs = await getExternalStorageDirectories(); return externalDirs[index]; } Future _saveMediaId(EpisodeTask episodeTask) async { final completeTask = await FlutterDownloader.loadTasksWithRawQuery( query: "SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'"); var filePath = 'file://${path.join(completeTask.first.savedDir, Uri.encodeComponent(completeTask.first.filename))}'; var fileStat = await File( path.join(completeTask.first.savedDir, completeTask.first.filename)) .stat(); await _dbHelper.saveMediaId(episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId, fileStat.size); _episodeTasks.removeWhere((element) => element.episode.enclosureUrl == episodeTask.episode.enclosureUrl); if (_episodeTasks.length == 0) _unbindBackgroundIsolate(); } Future startTask(List episodes, {bool showNotification = false}) async { for (var episode in episodes) { final dir = await _getDownloadDirectory(); var localPath = path.join(dir.path, episode.feedTitle); final saveDir = Directory(localPath); var hasExisted = await saveDir.exists(); if (!hasExisted) { saveDir.create(); } var now = DateTime.now(); var datePlus = now.year.toString() + now.month.toString() + now.day.toString() + now.second.toString(); var fileName = '${episode.title.replaceAll('/', '')}$datePlus.${episode.enclosureUrl.split('/').last.split('.').last}'; if (fileName.length > 100) { fileName = fileName.substring(fileName.length - 100); } var taskId = await FlutterDownloader.enqueue( fileName: fileName, url: episode.enclosureUrl, savedDir: localPath, showNotification: showNotification, openFileFromNotification: false, ); _episodeTasks.add(EpisodeTask(episode, taskId)); var dbHelper = DBHelper(); await dbHelper.saveDownloaded(episode.enclosureUrl, taskId); } await _completer.future; return; } } //For download episode inside app class DownloadState extends ChangeNotifier { final DBHelper _dbHelper = DBHelper(); List _episodeTasks = []; List get episodeTasks => _episodeTasks; DownloadState() { _autoDelete(); _bindBackgroundIsolate(); FlutterDownloader.registerCallback(downloadCallback); } @override void addListener(VoidCallback listener) async { _loadTasks(); super.addListener(listener); } Future _loadTasks() async { _episodeTasks = []; var dbHelper = DBHelper(); var tasks = await FlutterDownloader.loadTasks(); if (tasks.isNotEmpty) { for (var task in tasks) { var episode = await dbHelper.getRssItemWithUrl(task.url); if (episode == null) { await FlutterDownloader.remove( taskId: task.taskId, shouldDeleteContent: true); } else { if (task.status == DownloadTaskStatus.complete) { var exist = await File(path.join(task.savedDir, task.filename)).exists(); if (!exist) { await FlutterDownloader.remove( taskId: task.taskId, shouldDeleteContent: true); await dbHelper.delDownloaded(episode.enclosureUrl); } else { if (episode.enclosureUrl == episode.mediaId) { var filePath = 'file://${path.join(task.savedDir, Uri.encodeComponent(task.filename))}'; var fileStat = await File(path.join(task.savedDir, task.filename)).stat(); await dbHelper.saveMediaId( episode.enclosureUrl, filePath, task.taskId, fileStat.size); } _episodeTasks.add(EpisodeTask(episode, task.taskId, progress: task.progress, status: task.status)); } } else { _episodeTasks.add(EpisodeTask(episode, task.taskId, progress: task.progress, status: task.status)); } } } } notifyListeners(); } Future _getDownloadDirectory() async { final storage = KeyValueStorage(downloadPositionKey); final index = await storage.getInt(); final externalDirs = await getExternalStorageDirectories(); return externalDirs[index]; } void _bindBackgroundIsolate() { var _port = ReceivePort(); var isSuccess = IsolateNameServer.registerPortWithName( _port.sendPort, 'downloader_send_port'); if (!isSuccess) { _unbindBackgroundIsolate(); _bindBackgroundIsolate(); return; } _port.listen((dynamic data) { String id = data[0]; DownloadTaskStatus status = data[1]; int progress = data[2]; for (var episodeTask in _episodeTasks) { if (episodeTask.taskId == id) { episodeTask.status = status; episodeTask.progress = progress; if (status == DownloadTaskStatus.complete) { _saveMediaId(episodeTask).then((value) { notifyListeners(); }); } else { notifyListeners(); } } } }); } Future _saveMediaId(EpisodeTask episodeTask) async { episodeTask.status = DownloadTaskStatus.complete; final completeTask = await FlutterDownloader.loadTasksWithRawQuery( query: "SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'"); var filePath = 'file://${path.join(completeTask.first.savedDir, Uri.encodeComponent(completeTask.first.filename))}'; var fileStat = await File( path.join(completeTask.first.savedDir, completeTask.first.filename)) .stat(); _dbHelper.saveMediaId(episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId, fileStat.size); var episode = await _dbHelper.getRssItemWithUrl(episodeTask.episode.enclosureUrl); _removeTask(episodeTask.episode); _episodeTasks.add(EpisodeTask(episode, episodeTask.taskId, progress: 100, status: DownloadTaskStatus.complete)); } void _unbindBackgroundIsolate() { IsolateNameServer.removePortNameMapping('downloader_send_port'); } EpisodeTask episodeToTask(EpisodeBrief episode) { return _episodeTasks .firstWhere((task) => task.episode.enclosureUrl == episode.enclosureUrl, orElse: () { return EpisodeTask( episode, '', ); }); } @override void dispose() { _unbindBackgroundIsolate(); super.dispose(); } Future startTask(EpisodeBrief episode, {bool showNotification = true}) async { var isDownloaded = await _dbHelper.isDownloaded(episode.enclosureUrl); if (!isDownloaded) { final dir = await _getDownloadDirectory(); var localPath = path.join(dir.path, episode.feedTitle?.replaceAll('/', '')); final saveDir = Directory(localPath); var hasExisted = await saveDir.exists(); if (!hasExisted) { await saveDir.create(); } var now = DateTime.now(); var datePlus = now.year.toString() + now.month.toString() + now.day.toString() + now.second.toString(); var fileName = '${episode.title.replaceAll('/', '')}$datePlus.${episode.enclosureUrl.split('/').last.split('.').last}'; if (fileName.length > 100) { fileName = fileName.substring(fileName.length - 100); } var taskId = await FlutterDownloader.enqueue( fileName: fileName, url: episode.enclosureUrl, savedDir: localPath, showNotification: showNotification, openFileFromNotification: false, ); _episodeTasks.add(EpisodeTask(episode, taskId)); await _dbHelper.saveDownloaded(episode.enclosureUrl, taskId); notifyListeners(); } } Future pauseTask(EpisodeBrief episode) async { var task = episodeToTask(episode); if (task.progress > 0) { await FlutterDownloader.pause(taskId: task.taskId); } notifyListeners(); } Future resumeTask(EpisodeBrief episode) async { var task = episodeToTask(episode); var newTaskId = await FlutterDownloader.resume(taskId: task.taskId); await FlutterDownloader.remove(taskId: task.taskId); var index = _episodeTasks.indexOf(task); _episodeTasks[index] = task.copyWith(taskId: newTaskId); notifyListeners(); await _dbHelper.saveDownloaded(episode.enclosureUrl, newTaskId); } Future retryTask(EpisodeBrief episode) async { var task = episodeToTask(episode); var newTaskId = await FlutterDownloader.retry(taskId: task.taskId); await FlutterDownloader.remove(taskId: task.taskId); var index = _episodeTasks.indexOf(task); _episodeTasks[index] = task.copyWith(taskId: newTaskId); notifyListeners(); await _dbHelper.saveDownloaded(episode.enclosureUrl, newTaskId); } Future removeTask(EpisodeBrief episode) async { var task = episodeToTask(episode); await FlutterDownloader.remove( taskId: task.taskId, shouldDeleteContent: false); } Future delTask(EpisodeBrief episode) async { var task = episodeToTask(episode); await FlutterDownloader.remove( taskId: task.taskId, shouldDeleteContent: true); await _dbHelper.delDownloaded(episode.enclosureUrl); for (var episodeTask in _episodeTasks) { if (episodeTask.taskId == task.taskId) { episodeTask.status = DownloadTaskStatus.undefined; } notifyListeners(); } _removeTask(episode); } void _removeTask(EpisodeBrief episode) { _episodeTasks.removeWhere((element) => element.episode == episode); notifyListeners(); } Future _autoDelete() async { developer.log('Start auto delete outdated episodes'); final autoDeleteStorage = KeyValueStorage(autoDeleteKey); final deletePlayedStorage = KeyValueStorage(deleteAfterPlayedKey); final autoDelete = await autoDeleteStorage.getInt(); final deletePlayed = await deletePlayedStorage.getBool(defaultValue: false); if (autoDelete == 0) { await autoDeleteStorage.saveInt(30); } else if (autoDelete > 0) { var deadline = DateTime.now() .subtract(Duration(days: autoDelete)) .millisecondsSinceEpoch; var episodes = await _dbHelper.getOutdatedEpisode(deadline, deletePlayed: deletePlayed); if (episodes.isNotEmpty) { for (var episode in episodes) { await delTask(episode); } } final tasks = await FlutterDownloader.loadTasksWithRawQuery( query: 'SELECT * FROM task WHERE time_created < $deadline AND status = 3'); for (var task in tasks) { FlutterDownloader.remove( taskId: task.taskId, shouldDeleteContent: true); } } } }