From df9ae16d9eb8478fcb31ee68b5bc4d6e95262f55 Mon Sep 17 00:00:00 2001 From: Thomas Fiedler Date: Thu, 24 Nov 2022 08:47:54 +0100 Subject: [PATCH 1/4] Correctly read the error/success codes + Use the correct data type to evaluate the error codes when opening the SQLite database. --- ios/Classes/DBManager.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ios/Classes/DBManager.m b/ios/Classes/DBManager.m index 40f86c07..d1c91d1f 100644 --- a/ios/Classes/DBManager.m +++ b/ios/Classes/DBManager.m @@ -97,7 +97,13 @@ -(void)runQuery:(const char *)query isQueryExecutable:(BOOL)queryExecutable{ // Open the database. - BOOL openDatabaseResult = sqlite3_open([databasePath UTF8String], &sqlite3Database); + int openDatabaseResult = sqlite3_open([databasePath UTF8String], &sqlite3Database); + if(openDatabaseResult != SQLITE_OK) { + if(debug) { + NSLog(@"error opening the database with error no.: %d", openDatabaseResult); + } + return; + } if(openDatabaseResult == SQLITE_OK) { if (debug) { NSLog(@"open DB successfully"); @@ -107,7 +113,7 @@ -(void)runQuery:(const char *)query isQueryExecutable:(BOOL)queryExecutable{ sqlite3_stmt *compiledStatement; // Load all data from database to memory. - BOOL prepareStatementResult = sqlite3_prepare_v2(sqlite3Database, query, -1, &compiledStatement, NULL); + int prepareStatementResult = sqlite3_prepare_v2(sqlite3Database, query, -1, &compiledStatement, NULL); if(prepareStatementResult == SQLITE_OK) { // Check if the query is non-executable. if (!queryExecutable){ From 1ccd752e7d52ab636f46a24b82040ecf61c4fd1a Mon Sep 17 00:00:00 2001 From: Thomas Fiedler Date: Thu, 24 Nov 2022 10:34:49 +0100 Subject: [PATCH 2/4] Add schema migrations on iOS + Add a way to migrate the database schema on iOS + This partly reverts the changes to the db location --- ios/Classes/DBManager.m | 256 +++++++++++++++++++++++----------------- 1 file changed, 149 insertions(+), 107 deletions(-) diff --git a/ios/Classes/DBManager.m b/ios/Classes/DBManager.m index d1c91d1f..0de79c20 100644 --- a/ios/Classes/DBManager.m +++ b/ios/Classes/DBManager.m @@ -8,15 +8,18 @@ #import "DBManager.h" #import -@interface DBManager() +@interface DBManager () -@property (nonatomic, strong) NSString *appDirectory; -@property (nonatomic, strong) NSString *databaseFilePath; -@property (nonatomic, strong) NSString *databaseFilename; -@property (nonatomic, strong) NSMutableArray *arrResults; +@property(nonatomic, strong) NSString *appDirectory; +@property(nonatomic, strong) NSString *databaseFilePath; +@property(nonatomic, strong) NSString *databaseFilename; +@property(nonatomic, strong) NSMutableArray *arrResults; +@property(nonatomic) int version; +@property NSDictionary *migrations; --(void)copyDatabaseIntoAppDirectory; --(void)runQuery:(const char *)query isQueryExecutable:(BOOL)queryExecutable; +- (void)copyDatabaseIntoAppDirectory; + +- (void)runQuery:(const char *)query isQueryExecutable:(BOOL)queryExecutable; @end @@ -24,10 +27,13 @@ @implementation DBManager @synthesize debug; --(instancetype)initWithDatabaseFilePath:(NSString *)dbFilePath{ +// Version of the database +const int dbVersion = 1; + +- (instancetype)initWithDatabaseFilePath:(NSString *)dbFilePath { self = [super init]; if (self) { - self.appDirectory = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject]; + self.appDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; // Keep the database filepath self.databaseFilePath = dbFilePath; @@ -37,28 +43,76 @@ -(instancetype)initWithDatabaseFilePath:(NSString *)dbFilePath{ // Copy the database file into the app directory if necessary. [self copyDatabaseIntoAppDirectory]; + // run db migrations if necessary + self.version = dbVersion; + + // Keep track of migration code for the database + // To add a new migration, add a new entry to the dictionary, where the new db version is the key + // and the value will be a block that is executed when this migration is applied. + // Migrations will be applied in order. + self.migrations = @{ + @1: ^(DBManager *db) { + [db executeQuery:@"CREATE TABLE version(version INTEGER UNIQUE NOT NULL);"]; + [db executeQuery:@"INSERT INTO version(version) VALUES(1);"]; + }, + }; + [self migrateDatabase]; } return self; } +- (void)migrateDatabase { + int hasVersionTable = [[self getSingleRecord:@"SELECT EXISTS( SELECT 1 from sqlite_schema where tbl_name = 'version')"][0] intValue]; + + // DB does not yet have a version table, so we need to create it first. This is the very first migration we apply. + if (hasVersionTable == 0) { + ((void (^)(DBManager *)) self.migrations[@1])(self); + } + // in case the version table didn't exist yet, and we already have more than one db migration + // we follow the creation of the table up with further migrations. + // Otherwise, our work here is done, and we abort early. + if(self.version == 1) { + return; + } + NSArray *record = [self getSingleRecord:@"SELECT version from version LIMIT 1"]; + int version = [record[0] intValue]; + // nothing to do, move on + if (version == self.version) { + return; + } + + if (debug) { + NSLog(@"Migrating from %d to %d", version, self.version); + } + NSUInteger i; + for (i = (NSUInteger) version; i <= self.version; i++) { + void (^migration)(DBManager *) = (void (^)(DBManager *)) self.migrations[@(i + 1)]; + if (migration != nil) { + migration(self); + } + } + [self executeQuery:[NSString stringWithFormat:@"UPDATE version SET version = %d", self.version]]; +} + +// Executes a query to load data according to the query and unwraps the result to only be +// a single array with the actual result values. +- (NSArray *)getSingleRecord:(NSString *)forQuery { + NSArray *records = [[NSArray alloc] initWithArray:[self loadDataFromDB:forQuery]]; + NSArray *record = [records firstObject]; + + return record; +} + // Will be removed in the next major version. --(void)copyDatabaseIntoAppDirectory{ - // Check if the database file exists in the app directory. +- (void)copyDatabaseIntoAppDirectory { + // Check if the database file exists in the documents directory. NSString *destinationPath = [self.appDirectory stringByAppendingPathComponent:self.databaseFilename]; if (![[NSFileManager defaultManager] fileExistsAtPath:destinationPath]) { - - // Attemp database file migration from the documents directory if exists - NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; - NSString *migrationSourcePath = [documentsDirectory stringByAppendingPathComponent:self.databaseFilename]; + // The database file does not exist in the documents directory, so copy it from the main bundle now. + NSString *sourcePath = self.databaseFilePath; NSError *error; - if ([[NSFileManager defaultManager] fileExistsAtPath:migrationSourcePath]) { - // Migrate the database file from the documents directory to the app directory - [[NSFileManager defaultManager] moveItemAtPath:migrationSourcePath toPath:destinationPath error:&error]; - } else { - // The database file does not exist in the app directory, so copy it from the main bundle now. - [[NSFileManager defaultManager] copyItemAtPath:self.databaseFilePath toPath:destinationPath error:&error]; - } - + [[NSFileManager defaultManager] copyItemAtPath:sourcePath toPath:destinationPath error:&error]; + // Check if any error occurred during copying and display it. if (debug) { if (error != nil) { @@ -70,135 +124,123 @@ -(void)copyDatabaseIntoAppDirectory{ } } --(void)runQuery:(const char *)query isQueryExecutable:(BOOL)queryExecutable{ +- (void)runQuery:(const char *)query isQueryExecutable:(BOOL)queryExecutable { if (debug) { NSLog(@"execute query: %s", query); } // Create a sqlite object. sqlite3 *sqlite3Database; - + // Set the database file path. NSString *databasePath = [self.appDirectory stringByAppendingPathComponent:self.databaseFilename]; - + // Initialize the results array. if (self.arrResults != nil) { [self.arrResults removeAllObjects]; self.arrResults = nil; } self.arrResults = [[NSMutableArray alloc] init]; - + // Initialize the column names array. if (self.arrColumnNames != nil) { [self.arrColumnNames removeAllObjects]; self.arrColumnNames = nil; } self.arrColumnNames = [[NSMutableArray alloc] init]; - - + // Open the database. int openDatabaseResult = sqlite3_open([databasePath UTF8String], &sqlite3Database); - if(openDatabaseResult != SQLITE_OK) { - if(debug) { + if (openDatabaseResult != SQLITE_OK) { + if (debug) { NSLog(@"error opening the database with error no.: %d", openDatabaseResult); } return; } - if(openDatabaseResult == SQLITE_OK) { - if (debug) { - NSLog(@"open DB successfully"); - } - - // Declare a sqlite3_stmt object in which will be stored the query after having been compiled into a SQLite statement. - sqlite3_stmt *compiledStatement; - - // Load all data from database to memory. - int prepareStatementResult = sqlite3_prepare_v2(sqlite3Database, query, -1, &compiledStatement, NULL); - if(prepareStatementResult == SQLITE_OK) { - // Check if the query is non-executable. - if (!queryExecutable){ - // In this case data must be loaded from the database. - - // Declare an array to keep the data for each fetched row. - NSMutableArray *arrDataRow; - - // Loop through the results and add them to the results array row by row. - while(sqlite3_step(compiledStatement) == SQLITE_ROW) { - // Initialize the mutable array that will contain the data of a fetched row. - arrDataRow = [[NSMutableArray alloc] init]; - - // Get the total number of columns. - int totalColumns = sqlite3_column_count(compiledStatement); - - // Go through all columns and fetch each column data. - for (int i=0; i 0) { - [self.arrResults addObject:arrDataRow]; + + // Keep the current column name. + if (self.arrColumnNames.count != totalColumns) { + dbDataAsChars = (char *) sqlite3_column_name(compiledStatement, i); + [self.arrColumnNames addObject:[NSString stringWithUTF8String:dbDataAsChars]]; } } - } - else { - // This is the case of an executable query (insert, update, ...). - - // Execute the query. - int executeQueryResults = sqlite3_step(compiledStatement); - if (executeQueryResults == SQLITE_DONE) { - // Keep the affected rows. - self.affectedRows = sqlite3_changes(sqlite3Database); - - // Keep the last inserted row ID. - self.lastInsertedRowID = sqlite3_last_insert_rowid(sqlite3Database); + + // Store each fetched data row in the results array, but first check if there is actually data. + if (arrDataRow.count > 0) { + [self.arrResults addObject:arrDataRow]; } - else { - // If could not execute the query show the error message on the debugger. - if (debug) { - NSLog(@"DB Error: %s", sqlite3_errmsg(sqlite3Database)); - } + } + } else { + // This is the case of an executable query (insert, update, ...). + + // Execute the query. + int executeQueryResults = sqlite3_step(compiledStatement); + if (executeQueryResults == SQLITE_DONE) { + // Keep the affected rows. + self.affectedRows = sqlite3_changes(sqlite3Database); + + // Keep the last inserted row ID. + self.lastInsertedRowID = sqlite3_last_insert_rowid(sqlite3Database); + } else { + // If could not execute the query show the error message on the debugger. + if (debug) { + NSLog(@"DB Error: %s", sqlite3_errmsg(sqlite3Database)); } } } - else { - // In the database cannot be opened then show the error message on the debugger. - if (debug) { - NSLog(@"%s", sqlite3_errmsg(sqlite3Database)); - } + } else { + // In the database cannot be opened then show the error message on the debugger. + if (debug) { + NSLog(@"%s", sqlite3_errmsg(sqlite3Database)); } - - // Release the compiled statement from memory. - sqlite3_finalize(compiledStatement); } - + sqlite3_finalize(compiledStatement); + // Close the database. sqlite3_close(sqlite3Database); } --(NSArray *)loadDataFromDB:(NSString *)query{ +- (NSArray *)loadDataFromDB:(NSString *)query { // Run the query and indicate that is not executable. // The query string is converted to a char* object. [self runQuery:[query UTF8String] isQueryExecutable:NO]; - + // Returned the loaded results. - return (NSArray *)self.arrResults; + return (NSArray *) self.arrResults; } --(void)executeQuery:(NSString *)query{ +- (void)executeQuery:(NSString *)query { // Run the query and indicate that is executable. [self runQuery:[query UTF8String] isQueryExecutable:YES]; } From 54ac602bbb790e9230ec3cde13ae906953aaae48 Mon Sep 17 00:00:00 2001 From: Thomas Fiedler Date: Thu, 24 Nov 2022 11:40:07 +0100 Subject: [PATCH 3/4] #408 Add DB column for `allow_cellular` on iOS + Add a column to the table `task` + Add the value to the record when modeling tasks --- ios/Classes/DBManager.m | 5 ++++- ios/Classes/FlutterDownloaderPlugin.m | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ios/Classes/DBManager.m b/ios/Classes/DBManager.m index 0de79c20..c28c29ad 100644 --- a/ios/Classes/DBManager.m +++ b/ios/Classes/DBManager.m @@ -28,7 +28,7 @@ @implementation DBManager @synthesize debug; // Version of the database -const int dbVersion = 1; +const int dbVersion = 2; - (instancetype)initWithDatabaseFilePath:(NSString *)dbFilePath { self = [super init]; @@ -55,6 +55,9 @@ - (instancetype)initWithDatabaseFilePath:(NSString *)dbFilePath { [db executeQuery:@"CREATE TABLE version(version INTEGER UNIQUE NOT NULL);"]; [db executeQuery:@"INSERT INTO version(version) VALUES(1);"]; }, + @2: ^(DBManager *db) { + [db executeQuery:@"ALTER TABLE task ADD COLUMN allow_cellular INTEGER DEFAULT TRUE;"]; + } }; [self migrateDatabase]; } diff --git a/ios/Classes/FlutterDownloaderPlugin.m b/ios/Classes/FlutterDownloaderPlugin.m index 2c2aefb3..f1bac9b4 100644 --- a/ios/Classes/FlutterDownloaderPlugin.m +++ b/ios/Classes/FlutterDownloaderPlugin.m @@ -23,6 +23,7 @@ #define KEY_OPEN_FILE_FROM_NOTIFICATION @"open_file_from_notification" #define KEY_QUERY @"query" #define KEY_TIME_CREATED @"time_created" +#define KEY_ALLOW_CELLULAR @"allow_cellular" #define NULL_VALUE @"" @@ -588,7 +589,9 @@ - (NSDictionary*) taskDictFromRecordArray:(NSArray*)record int showNotification = [[record objectAtIndex:[_dbManager.arrColumnNames indexOfObject:@"show_notification"]] intValue]; int openFileFromNotification = [[record objectAtIndex:[_dbManager.arrColumnNames indexOfObject:@"open_file_from_notification"]] intValue]; long long timeCreated = [[record objectAtIndex:[_dbManager.arrColumnNames indexOfObject:@"time_created"]] longLongValue]; - return [NSDictionary dictionaryWithObjectsAndKeys:taskId, KEY_TASK_ID, @(status), KEY_STATUS, @(progress), KEY_PROGRESS, url, KEY_URL, filename, KEY_FILE_NAME, headers, KEY_HEADERS, savedDir, KEY_SAVED_DIR, [NSNumber numberWithBool:(resumable == 1)], KEY_RESUMABLE, [NSNumber numberWithBool:(showNotification == 1)], KEY_SHOW_NOTIFICATION, [NSNumber numberWithBool:(openFileFromNotification == 1)], KEY_OPEN_FILE_FROM_NOTIFICATION, @(timeCreated), KEY_TIME_CREATED, nil]; + int allowCellular = [[record objectAtIndex:[_dbManager.arrColumnNames indexOfObject:@"allow_cellular"]] intValue]; + + return [NSDictionary dictionaryWithObjectsAndKeys:taskId, KEY_TASK_ID, @(status), KEY_STATUS, @(progress), KEY_PROGRESS, url, KEY_URL, filename, KEY_FILE_NAME, headers, KEY_HEADERS, savedDir, KEY_SAVED_DIR, [NSNumber numberWithBool:(resumable == 1)], KEY_RESUMABLE, [NSNumber numberWithBool:(showNotification == 1)], KEY_SHOW_NOTIFICATION, [NSNumber numberWithBool:(openFileFromNotification == 1)], KEY_OPEN_FILE_FROM_NOTIFICATION, @(timeCreated), KEY_TIME_CREATED, [NSNumber numberWithBool:(allowCellular == 1)], KEY_ALLOW_CELLULAR, nil]; } @catch(NSException *exception) { NSLog(@"invalid task data: %@", exception); return [NSDictionary dictionary]; From 7fec9a6e3c76337d98eaeeed6a45800e59a32502 Mon Sep 17 00:00:00 2001 From: Thomas Fiedler Date: Fri, 25 Nov 2022 12:47:32 +0100 Subject: [PATCH 4/4] #408 Use different NSURLSessions to prevent downloads via cellular data + iOS handles this on a session basis and not on a per task basis, so we have to configure two different sessions. One that allows downloading via cellular data and one that doesn't. --- example/lib/home_page.dart | 39 +++++++------ ios/Classes/FlutterDownloaderPlugin.m | 83 ++++++++++++++++----------- 2 files changed, 72 insertions(+), 50 deletions(-) diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 6b2e0b0b..46747203 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -28,6 +28,7 @@ class _MyHomePageState extends State { late bool _showContent; late bool _permissionReady; late bool _saveInPublicStorage; + late bool _allowCellular; late String _localPath; final ReceivePort _port = ReceivePort(); @@ -42,6 +43,7 @@ class _MyHomePageState extends State { _showContent = false; _permissionReady = false; _saveInPublicStorage = false; + _allowCellular = true; _prepare(); } @@ -98,8 +100,7 @@ class _MyHomePageState extends State { 'task ($id) is in status ($status) and process ($progress)', ); - IsolateNameServer.lookupPortByName('downloader_send_port') - ?.send([id, status, progress]); + IsolateNameServer.lookupPortByName('downloader_send_port')?.send([id, status, progress]); } Widget _buildDownloadList() { @@ -117,13 +118,23 @@ class _MyHomePageState extends State { const Text('Save in public storage'), ], ), + Row( + children: [ + Checkbox( + value: _allowCellular, + onChanged: (newValue) { + setState(() => _allowCellular = newValue ?? true); + }, + ), + const Text('Allow download via cellular data'), + ], + ), ..._items.map( (item) { final task = item.task; if (task == null) { return Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text( item.name!, style: const TextStyle( @@ -154,8 +165,7 @@ class _MyHomePageState extends State { _pauseDownload(task); } else if (task.status == DownloadTaskStatus.paused) { _resumeDownload(task); - } else if (task.status == DownloadTaskStatus.complete || - task.status == DownloadTaskStatus.canceled) { + } else if (task.status == DownloadTaskStatus.complete || task.status == DownloadTaskStatus.canceled) { _delete(task); } else if (task.status == DownloadTaskStatus.failed) { _retryDownload(task); @@ -217,6 +227,7 @@ class _MyHomePageState extends State { headers: {'auth': 'test_for_sql_encoding'}, savedDir: _localPath, saveInPublicStorage: _saveInPublicStorage, + allowCellular: _allowCellular, ); } @@ -300,8 +311,7 @@ class _MyHomePageState extends State { } _tasks!.addAll( - DownloadItems.images - .map((image) => TaskInfo(name: image.name, link: image.url)), + DownloadItems.images.map((image) => TaskInfo(name: image.name, link: image.url)), ); _items.add(ItemHolder(name: 'Images')); @@ -311,8 +321,7 @@ class _MyHomePageState extends State { } _tasks!.addAll( - DownloadItems.videos - .map((video) => TaskInfo(name: video.name, link: video.url)), + DownloadItems.videos.map((video) => TaskInfo(name: video.name, link: video.url)), ); _items.add(ItemHolder(name: 'Videos')); @@ -322,8 +331,7 @@ class _MyHomePageState extends State { } _tasks!.addAll( - DownloadItems.apks - .map((video) => TaskInfo(name: video.name, link: video.url)), + DownloadItems.apks.map((video) => TaskInfo(name: video.name, link: video.url)), ); _items.add(ItemHolder(name: 'APKs')); @@ -374,8 +382,7 @@ class _MyHomePageState extends State { externalStorageDirPath = directory?.path; } } else if (Platform.isIOS) { - externalStorageDirPath = - (await getApplicationDocumentsDirectory()).absolute.path; + externalStorageDirPath = (await getApplicationDocumentsDirectory()).absolute.path; } return externalStorageDirPath; } @@ -412,9 +419,7 @@ class _MyHomePageState extends State { return const Center(child: CircularProgressIndicator()); } - return _permissionReady - ? _buildDownloadList() - : _buildNoPermissionWarning(); + return _permissionReady ? _buildDownloadList() : _buildNoPermissionWarning(); }, ), ); diff --git a/ios/Classes/FlutterDownloaderPlugin.m b/ios/Classes/FlutterDownloaderPlugin.m index f1bac9b4..7d3af6e0 100644 --- a/ios/Classes/FlutterDownloaderPlugin.m +++ b/ios/Classes/FlutterDownloaderPlugin.m @@ -53,6 +53,7 @@ @implementation FlutterDownloaderPlugin static BOOL initialized = NO; static BOOL debug = YES; static NSURLSession *_session = nil; +static NSURLSession *_wifiOnlySession = nil; static FlutterEngine *_headlessRunner = nil; static int64_t _callbackHandle = 0; static int _step = 10; @@ -93,7 +94,7 @@ - (instancetype)init:(NSObject *)registrar; if (debug) { NSLog(@"database path: %@", dbPath); } - databaseQueue = dispatch_queue_create("vn.hunghd.flutter_downloader", 0); + databaseQueue = dispatch_queue_create("vn.hunghd.flutter_downloader", nil); _dbManager = [[DBManager alloc] initWithDatabaseFilePath:dbPath]; if (_runningTaskById == nil) { @@ -111,13 +112,22 @@ - (instancetype)init:(NSObject *)registrar; if (debug) { NSLog(@"MAXIMUM_CONCURRENT_TASKS = %@", maxConcurrentTasks); } + + NSString *wifiIdentifier = [NSString stringWithFormat:@"%@.download.background.session.wifi", NSBundle.mainBundle.bundleIdentifier]; + NSURLSessionConfiguration *wifiSessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:wifiIdentifier]; + wifiSessionConfiguration.HTTPMaximumConnectionsPerHost = [maxConcurrentTasks intValue]; + wifiSessionConfiguration.allowsCellularAccess = NO; + _wifiOnlySession = [NSURLSession sessionWithConfiguration:wifiSessionConfiguration delegate:self delegateQueue:nil]; + // session identifier needs to be the same for background download and resume to work NSString *identifier = [NSString stringWithFormat:@"%@.download.background.session", NSBundle.mainBundle.bundleIdentifier]; NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier]; sessionConfiguration.HTTPMaximumConnectionsPerHost = [maxConcurrentTasks intValue]; _session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil]; + if (debug) { NSLog(@"init NSURLSession with id: %@", [[_session configuration] identifier]); + NSLog(@"init wifi-only NSURLSession with id: %@", [[_wifiOnlySession configuration] identifier]); } } @@ -156,11 +166,11 @@ - (FlutterMethodChannel *)channel { return _mainChannel; } -- (NSURLSession*)currentSession { - return _session; +- (NSURLSession*)currentSession: (bool) allowCellular { + return allowCellular ? _session : _wifiOnlySession; } -- (NSURLSessionDownloadTask*)downloadTaskWithURL: (NSURL*) url fileName: (NSString*) fileName andSavedDir: (NSString*) savedDir andHeaders: (NSString*) headers +- (NSURLSessionDownloadTask*)downloadTaskWithURL: (NSURL*) url fileName: (NSString*) fileName andSavedDir: (NSString*) savedDir andHeaders: (NSString*) headers allowCellular: (bool) allowCellular { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; if (headers != nil && [headers length] > 0) { @@ -176,7 +186,7 @@ - (NSURLSessionDownloadTask*)downloadTaskWithURL: (NSURL*) url fileName: (NSStri [request setValue:value forHTTPHeaderField:key]; } } - NSURLSessionDownloadTask *task = [[self currentSession] downloadTaskWithRequest:request]; + NSURLSessionDownloadTask *task = [[self currentSession:allowCellular ] downloadTaskWithRequest:request]; // store task id in taskDescription task.taskDescription = [self createTaskId]; [task resume]; @@ -194,30 +204,32 @@ - (NSString*)identifierForTask:(NSURLSessionTask*) task return task.taskDescription; } -- (NSString*)identifierForTask:(NSURLSessionTask*) task ofSession:(NSURLSession *)session -{ - return task.taskDescription; -} - - (void)updateRunningTaskById:(NSString*)taskId progress:(int)progress status:(int)status resumable:(BOOL)resumable { _runningTaskById[taskId][KEY_PROGRESS] = @(progress); _runningTaskById[taskId][KEY_STATUS] = @(status); _runningTaskById[taskId][KEY_RESUMABLE] = @(resumable); } +- (void) applyToTask: (void (NS_SWIFT_SENDABLE ^)(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks))handler { + // apply handler to tasks from wifi-only session + [[self currentSession:false] getTasksWithCompletionHandler:handler]; + // apply to regular session tasks + [[self currentSession:true] getTasksWithCompletionHandler:handler]; +} + - (void)pauseTaskWithId: (NSString*)taskId { if (debug) { NSLog(@"pause task with id: %@", taskId); } __typeof__(self) __weak weakSelf = self; - [[self currentSession] getTasksWithCompletionHandler:^(NSArray *data, NSArray *uploads, NSArray *downloads) { + [ self applyToTask:^(NSArray *data, NSArray *uploads, NSArray *downloads) { for (NSURLSessionDownloadTask *download in downloads) { NSURLSessionTaskState state = download.state; NSString *taskIdValue = [weakSelf identifierForTask:download]; if ([taskId isEqualToString:taskIdValue] && (state == NSURLSessionTaskStateRunning)) { NSDictionary *task = [weakSelf loadTaskWithId:taskIdValue]; - + int progress = 0; if (download.countOfBytesExpectedToReceive > 0) { int64_t bytesReceived = download.countOfBytesReceived; @@ -227,7 +239,7 @@ - (void)pauseTaskWithId: (NSString*)taskId NSNumber *progressNumOfTask = task[@"progress"]; progress = progressNumOfTask.intValue; } - + [download cancelByProducingResumeData:^(NSData * _Nullable resumeData) { // Save partial downloaded data to a file NSFileManager *fileManager = [NSFileManager defaultManager]; @@ -262,7 +274,7 @@ - (void)cancelTaskWithId: (NSString*)taskId NSLog(@"cancel task with id: %@", taskId); } __typeof__(self) __weak weakSelf = self; - [[self currentSession] getTasksWithCompletionHandler:^(NSArray *data, NSArray *uploads, NSArray *downloads) { + [self applyToTask:^(NSArray *data, NSArray *uploads, NSArray *downloads) { for (NSURLSessionDownloadTask *download in downloads) { NSURLSessionTaskState state = download.state; NSString *taskIdValue = [self identifierForTask:download]; @@ -280,7 +292,7 @@ - (void)cancelTaskWithId: (NSString*)taskId - (void)cancelAllTasks { __typeof__(self) __weak weakSelf = self; - [[self currentSession] getTasksWithCompletionHandler:^(NSArray *data, NSArray *uploads, NSArray *downloads) { + [self applyToTask:^(NSArray *data, NSArray *uploads, NSArray *downloads) { for (NSURLSessionDownloadTask *download in downloads) { NSURLSessionTaskState state = download.state; if (state == NSURLSessionTaskStateRunning) { @@ -643,22 +655,26 @@ - (void)enqueueMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result NSString *headers = call.arguments[KEY_HEADERS]; NSNumber *showNotification = call.arguments[KEY_SHOW_NOTIFICATION]; NSNumber *openFileFromNotification = call.arguments[KEY_OPEN_FILE_FROM_NOTIFICATION]; + bool allowCellular = [call.arguments[KEY_ALLOW_CELLULAR] boolValue]; - NSURLSessionDownloadTask *task = [self downloadTaskWithURL:[NSURL URLWithString:urlString] fileName:fileName andSavedDir:savedDir andHeaders:headers]; + NSURLSessionDownloadTask *task = [self downloadTaskWithURL:[NSURL URLWithString:urlString] fileName:fileName andSavedDir:savedDir andHeaders:headers allowCellular:allowCellular]; NSString *taskId = [self identifierForTask:task]; - [_runningTaskById setObject: [NSMutableDictionary dictionaryWithObjectsAndKeys: - urlString, KEY_URL, - fileName, KEY_FILE_NAME, - savedDir, KEY_SAVED_DIR, - headers, KEY_HEADERS, - showNotification, KEY_SHOW_NOTIFICATION, - openFileFromNotification, KEY_OPEN_FILE_FROM_NOTIFICATION, - @(NO), KEY_RESUMABLE, - @(STATUS_ENQUEUED), KEY_STATUS, - @(0), KEY_PROGRESS, nil] - forKey:taskId]; + NSMutableDictionary *taskDict = [NSMutableDictionary dictionaryWithObjectsAndKeys: + urlString, KEY_URL, + fileName, KEY_FILE_NAME, + savedDir, KEY_SAVED_DIR, + headers, KEY_HEADERS, + showNotification, KEY_SHOW_NOTIFICATION, + openFileFromNotification, KEY_OPEN_FILE_FROM_NOTIFICATION, + @(NO), KEY_RESUMABLE, + @(STATUS_ENQUEUED), KEY_STATUS, + @(0), KEY_PROGRESS, + [NSNumber numberWithBool:allowCellular], KEY_ALLOW_CELLULAR, + nil]; + + _runningTaskById[taskId] = taskDict; __typeof__(self) __weak weakSelf = self; @@ -718,7 +734,7 @@ - (void)resumeMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSData *resumeData = [NSData dataWithContentsOfURL:partialFileURL]; if (resumeData != nil) { - NSURLSessionDownloadTask *task = [[self currentSession] downloadTaskWithResumeData:resumeData]; + NSURLSessionDownloadTask *task = [[self currentSession:(bool) taskDict[KEY_ALLOW_CELLULAR]] downloadTaskWithResumeData:resumeData]; NSString *newTaskId = [self createTaskId]; task.taskDescription = newTaskId; [task resume]; @@ -764,8 +780,9 @@ - (void)retryMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *savedDir = taskDict[KEY_SAVED_DIR]; NSString *fileName = taskDict[KEY_FILE_NAME]; NSString *headers = taskDict[KEY_HEADERS]; + bool allowCellular = [taskDict[KEY_ALLOW_CELLULAR] boolValue]; - NSURLSessionDownloadTask *newTask = [self downloadTaskWithURL:[NSURL URLWithString:urlString] fileName:fileName andSavedDir:savedDir andHeaders:headers]; + NSURLSessionDownloadTask *newTask = [self downloadTaskWithURL:[NSURL URLWithString:urlString] fileName:fileName andSavedDir:savedDir andHeaders:headers allowCellular: allowCellular]; NSString *newTaskId = [self identifierForTask:newTask]; // update memory-cache @@ -821,7 +838,7 @@ - (void)removeMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if (taskDict != nil) { NSNumber* status = taskDict[KEY_STATUS]; if ([status intValue] == STATUS_ENQUEUED || [status intValue] == STATUS_RUNNING) { - [[self currentSession] getTasksWithCompletionHandler:^(NSArray *data, NSArray *uploads, NSArray *downloads) { + [self applyToTask:^(NSArray *data, NSArray *uploads, NSArray *downloads) { for (NSURLSessionDownloadTask *download in downloads) { NSURLSessionTaskState state = download.state; NSString *taskIdValue = [weakSelf identifierForTask:download]; @@ -956,7 +973,7 @@ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTas bool isSuccess = (httpStatusCode >= 200 && httpStatusCode < 300); if (isSuccess) { - NSString *taskId = [self identifierForTask:downloadTask ofSession:session]; + NSString *taskId = [self identifierForTask:downloadTask]; NSDictionary *task = [self loadTaskWithId:taskId]; NSURL *destinationURL = [self fileUrlOf:taskId taskInfo:task downloadTask:downloadTask]; @@ -1007,7 +1024,7 @@ -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompl if (debug) { NSLog(@"Download completed with error: %@", error != nil ? [error localizedDescription] : @(httpStatusCode)); } - NSString *taskId = [self identifierForTask:task ofSession:session]; + NSString *taskId = [self identifierForTask:task]; NSDictionary *taskInfo = [self loadTaskWithId:taskId]; NSNumber *resumable = taskInfo[KEY_RESUMABLE]; if (![resumable boolValue]) { @@ -1033,7 +1050,7 @@ -(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session NSLog(@"URLSessionDidFinishEventsForBackgroundURLSession:"); } // Check if all download tasks have been finished. - [[self currentSession] getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { + [self applyToTask:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { if ([downloadTasks count] == 0) { if (debug) { NSLog(@"all download tasks have been finished");