diff --git a/platform/darwin/include/MGLOfflinePack.h b/platform/darwin/include/MGLOfflinePack.h index d1f5a3ed5..fd8080523 100644 --- a/platform/darwin/include/MGLOfflinePack.h +++ b/platform/darwin/include/MGLOfflinePack.h @@ -148,16 +148,6 @@ typedef struct MGLOfflinePackProgress { */ - (void)suspend; -/** - Request an asynchronous update to the pack’s `state` and `progress` properties. - - The state and progress of an inactive or completed pack are computed lazily. If - you need the state or progress of a pack inside an - `MGLOfflinePackListingCompletionHandler`, set the `delegate` property then call - this method. - */ -- (void)requestProgress; - @end /** diff --git a/platform/darwin/include/MGLOfflineStorage.h b/platform/darwin/include/MGLOfflineStorage.h index 4f3432163..931982dce 100644 --- a/platform/darwin/include/MGLOfflineStorage.h +++ b/platform/darwin/include/MGLOfflineStorage.h @@ -7,6 +7,13 @@ NS_ASSUME_NONNULL_BEGIN @class MGLOfflinePack; @protocol MGLOfflineRegion; +extern NSString * const MGLOfflinePackProgressChangedNotification; +extern NSString * const MGLOfflinePackErrorNotification; +extern NSString * const MGLOfflinePackMaximumMapboxTilesReachedNotification; + +extern NSString * const MGLOfflinePackErrorUserInfoKey; +extern NSString * const MGLOfflinePackMaximumCountUserInfoKey; + /** A block to be called once an offline pack has been completely created and added. @@ -28,16 +35,6 @@ typedef void (^MGLOfflinePackAdditionCompletionHandler)(MGLOfflinePack * _Nullab */ typedef void (^MGLOfflinePackRemovalCompletionHandler)(NSError * _Nullable error); -/** - A block to be called with a complete list of offline packs. - - @param pack Contains a pointer an array of packs, or `nil` if there was an - error obtaining the packs. - @param error Contains a pointer to an error object (if any) indicating why the - list of packs could not be obtained. - */ -typedef void (^MGLOfflinePackListingCompletionHandler)(NS_ARRAY_OF(MGLOfflinePack *) *packs, NSError * _Nullable error); - /** MGLOfflineStorage implements a singleton (shared object) that manages offline packs. All of this class’s instance methods are asynchronous, reflecting the @@ -50,7 +47,16 @@ typedef void (^MGLOfflinePackListingCompletionHandler)(NS_ARRAY_OF(MGLOfflinePac */ + (instancetype)sharedOfflineStorage; -- (instancetype)init NS_UNAVAILABLE; +/** + An array of all known offline packs. + + This property is set to `nil`, indicating that the receiver does not yet know + the existing packs, for an undefined amount of time starting from the moment + the shared offline storage object is initialized until the packs are fetched + from the database. After that point, this property is always non-nil, but it + may be empty to indicate that no packs are present. + */ +@property (nonatomic, copy, readonly, nullable) NS_ARRAY_OF(MGLOfflinePack *) *packs; /** Creates and registers an offline pack that downloads the resources needed to @@ -84,14 +90,6 @@ typedef void (^MGLOfflinePackListingCompletionHandler)(NS_ARRAY_OF(MGLOfflinePac */ - (void)removePack:(MGLOfflinePack *)pack withCompletionHandler:(nullable MGLOfflinePackRemovalCompletionHandler)completion; -/** - Asynchronously calls a completion callback with all existing offline packs. - - @param completion The completion handler to call with the list of packs. This - handler is executed asynchronously on the main queue. - */ -- (void)getPacksWithCompletionHandler:(MGLOfflinePackListingCompletionHandler)completion; - /** Sets the maximum number of Mapbox-hosted tiles that may be downloaded and stored on the current device. diff --git a/platform/darwin/src/MGLOfflinePack_Private.h b/platform/darwin/src/MGLOfflinePack_Private.h index e29018fc3..c1637ab4a 100644 --- a/platform/darwin/src/MGLOfflinePack_Private.h +++ b/platform/darwin/src/MGLOfflinePack_Private.h @@ -10,6 +10,16 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithMBGLRegion:(mbgl::OfflineRegion *)region; +/** + Request an asynchronous update to the pack’s `state` and `progress` properties. + + The state and progress of an inactive or completed pack are computed lazily. If + you need the state or progress of a pack inside an + `MGLOfflinePackListingCompletionHandler`, set the `delegate` property then call + this method. + */ +- (void)requestProgress; + /** Invalidates the pack and ensures that no future progress update can ever revalidate it. diff --git a/platform/darwin/src/MGLOfflineStorage.mm b/platform/darwin/src/MGLOfflineStorage.mm index 4dbfb0580..2add5e105 100644 --- a/platform/darwin/src/MGLOfflineStorage.mm +++ b/platform/darwin/src/MGLOfflineStorage.mm @@ -8,12 +8,31 @@ #include -@interface MGLOfflineStorage () +static NSString * const MGLOfflineStorageFileName = @"cache.db"; +static NSString * const MGLOfflineStorageFileName3_2_0_beta_1 = @"offline.db"; +NSString * const MGLOfflinePackProgressChangedNotification = @"MGLOfflinePackProgressChanged"; +NSString * const MGLOfflinePackErrorNotification = @"MGLOfflinePackError"; +NSString * const MGLOfflinePackMaximumMapboxTilesReachedNotification = @"MGLOfflinePackMaximumMapboxTilesReached"; + +NSString * const MGLOfflinePackErrorUserInfoKey = @"Error"; +NSString * const MGLOfflinePackMaximumCountUserInfoKey = @"MaximumCount"; + +/** + A block to be called with a complete list of offline packs. + + @param pack Contains a pointer an array of packs, or `nil` if there was an + error obtaining the packs. + @param error Contains a pointer to an error object (if any) indicating why the + list of packs could not be obtained. + */ +typedef void (^MGLOfflinePackListingCompletionHandler)(NS_ARRAY_OF(MGLOfflinePack *) *packs, NSError * _Nullable error); + +@interface MGLOfflineStorage () + +@property (nonatomic, copy, readwrite) NS_MUTABLE_ARRAY_OF(MGLOfflinePack *) *packs; @property (nonatomic) mbgl::DefaultFileSource *mbglFileSource; -- (instancetype)initWithFileName:(NSString *)fileName NS_DESIGNATED_INITIALIZER; - @end @implementation MGLOfflineStorage @@ -22,14 +41,20 @@ static dispatch_once_t onceToken; static MGLOfflineStorage *sharedOfflineStorage; dispatch_once(&onceToken, ^{ - sharedOfflineStorage = [[self alloc] initWithFileName:@"cache.db"]; + sharedOfflineStorage = [[self alloc] init]; + [sharedOfflineStorage getPacksWithCompletionHandler:^(NS_ARRAY_OF(MGLOfflinePack *) *packs, __unused NSError *error) { + sharedOfflineStorage.packs = [packs mutableCopy]; + + for (MGLOfflinePack *pack in packs) { + pack.delegate = sharedOfflineStorage; + [pack requestProgress]; + } + }]; }); return sharedOfflineStorage; } -// This method can’t be called -init, because that selector has been marked -// unavailable in MGLOfflineStorage.h. -- (instancetype)initWithFileName:(NSString *)fileName { +- (instancetype)init { if (self = [super init]) { // Place the cache in a location specific to the application, so that // packs downloaded by other applications don’t count toward this @@ -46,7 +71,7 @@ withIntermediateDirectories:YES attributes:nil error:nil]; - NSURL *cacheURL = [cacheDirectoryURL URLByAppendingPathComponent:fileName]; + NSURL *cacheURL = [cacheDirectoryURL URLByAppendingPathComponent:MGLOfflineStorageFileName]; NSString *cachePath = cacheURL ? cacheURL.path : @""; // Avoid backing up the offline cache onto iCloud, because it can be @@ -57,11 +82,10 @@ // Move the offline cache from v3.2.0-beta.1 to a location that can also // be used for ambient caching. - NSString *legacyCacheFileName = @"offline.db"; #if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR // ~/Documents/offline.db NSArray *legacyPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *legacyCachePath = [legacyPaths.firstObject stringByAppendingPathComponent:legacyCacheFileName]; + NSString *legacyCachePath = [legacyPaths.firstObject stringByAppendingPathComponent:MGLOfflineStorageFileName3_2_0_beta_1]; #elif TARGET_OS_MAC // ~/Library/Caches/tld.app.bundle.id/offline.db NSURL *legacyCacheDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory @@ -75,7 +99,7 @@ withIntermediateDirectories:YES attributes:nil error:nil]; - NSURL *legacyCacheURL = [legacyCacheDirectoryURL URLByAppendingPathComponent:legacyCacheFileName]; + NSURL *legacyCacheURL = [legacyCacheDirectoryURL URLByAppendingPathComponent:MGLOfflineStorageLegacyFileName]; NSString *legacyCachePath = legacyCacheURL ? legacyCacheURL.path : @""; #endif if (![[NSFileManager defaultManager] fileExistsAtPath:cachePath]) { @@ -101,17 +125,52 @@ _mbglFileSource = nullptr; } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NS_DICTIONARY_OF(NSString *, id) *)change context:(__unused void *)context { +#pragma mark KVO methods + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NS_DICTIONARY_OF(NSString *, id) *)change context:(void *)context { // Synchronize the file source’s access token with the global one in MGLAccountManager. if ([keyPath isEqualToString:@"accessToken"] && object == [MGLAccountManager sharedManager]) { NSString *accessToken = change[NSKeyValueChangeNewKey]; if (![accessToken isKindOfClass:[NSNull class]]) { self.mbglFileSource->setAccessToken(accessToken.UTF8String); } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } +- (void)insertPacks:(NSArray *)packs atIndexes:(NSIndexSet *)indices { + [(NSMutableArray *)self.packs insertObjects:packs atIndexes:indices]; +} + +- (void)addPacksObject:(MGLOfflinePack *)pack { + [(NSMutableArray *)self.packs addObject:pack]; +} + +- (void)removePacksAtIndexes:(NSIndexSet *)indices { + [(NSMutableArray *)self.packs removeObjectsAtIndexes:indices]; +} + +- (void)removePacksObject:(MGLOfflinePack *)pack { + [(NSMutableArray *)self.packs removeObject:pack]; +} + +#pragma mark Pack management methods + - (void)addPackForRegion:(id )region withContext:(NSData *)context completionHandler:(MGLOfflinePackAdditionCompletionHandler)completion { + __weak MGLOfflineStorage *weakSelf = self; + [self _addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack * _Nullable pack, NSError * _Nullable error) { + MGLOfflineStorage *strongSelf = weakSelf; + [strongSelf addPacksObject:pack]; + pack.delegate = strongSelf; + [pack requestProgress]; + if (completion) { + completion(pack, error); + } + }]; +} + +- (void)_addPackForRegion:(id )region withContext:(NSData *)context completionHandler:(MGLOfflinePackAdditionCompletionHandler)completion { if (![region conformsToProtocol:@protocol(MGLOfflineRegion_Private)]) { [NSException raise:@"Unsupported region type" format: @"Regions of type %@ are unsupported.", NSStringFromClass([region class])]; @@ -139,6 +198,15 @@ } - (void)removePack:(MGLOfflinePack *)pack withCompletionHandler:(MGLOfflinePackRemovalCompletionHandler)completion { + [self removePacksObject:pack]; + [self _removePack:pack withCompletionHandler:^(NSError * _Nullable error) { + if (completion) { + completion(error); + } + }]; +} + +- (void)_removePack:(MGLOfflinePack *)pack withCompletionHandler:(MGLOfflinePackRemovalCompletionHandler)completion { mbgl::OfflineRegion *mbglOfflineRegion = pack.mbglOfflineRegion; [pack invalidate]; self.mbglFileSource->deleteOfflineRegion(std::move(*mbglOfflineRegion), [&, completion](std::exception_ptr exception) { @@ -184,4 +252,22 @@ _mbglFileSource->setOfflineMapboxTileCountLimit(maximumCount); } +#pragma mark MGLOfflinePackDelegate methods + +- (void)offlinePack:(MGLOfflinePack *)pack progressDidChange:(__unused MGLOfflinePackProgress)progress { + [[NSNotificationCenter defaultCenter] postNotificationName:MGLOfflinePackProgressChangedNotification object:pack]; +} + +- (void)offlinePack:(MGLOfflinePack *)pack didReceiveError:(NSError *)error { + [[NSNotificationCenter defaultCenter] postNotificationName:MGLOfflinePackErrorNotification object:pack userInfo:@{ + MGLOfflinePackErrorUserInfoKey: error, + }]; +} + +- (void)offlinePack:(MGLOfflinePack *)pack didReceiveMaximumAllowedMapboxTiles:(uint64_t)maximumCount { + [[NSNotificationCenter defaultCenter] postNotificationName:MGLOfflinePackMaximumMapboxTilesReachedNotification object:pack userInfo:@{ + MGLOfflinePackMaximumCountUserInfoKey: @(maximumCount), + }]; +} + @end diff --git a/platform/ios/app/MBXOfflinePacksTableViewController.m b/platform/ios/app/MBXOfflinePacksTableViewController.m index fb9d5fb3e..4f399d3d4 100644 --- a/platform/ios/app/MBXOfflinePacksTableViewController.m +++ b/platform/ios/app/MBXOfflinePacksTableViewController.m @@ -28,12 +28,6 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac @end -@interface MBXOfflinePacksTableViewController () - -@property (nonatomic, strong) NS_MUTABLE_ARRAY_OF(MGLOfflinePack *) *offlinePacks; - -@end - @implementation MBXOfflinePacksTableViewController { NSUInteger _untitledRegionCount; } @@ -41,25 +35,48 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac - (void)viewDidLoad { [super viewDidLoad]; - __weak MBXOfflinePacksTableViewController *weakSelf = self; - [[MGLOfflineStorage sharedOfflineStorage] getPacksWithCompletionHandler:^(NS_ARRAY_OF(MGLOfflinePack *) *packs, NSError *error) { - MBXOfflinePacksTableViewController *strongSelf = weakSelf; - strongSelf.offlinePacks = packs.mutableCopy; - [strongSelf.tableView reloadData]; - - for (MGLOfflinePack *pack in strongSelf.offlinePacks) { - pack.delegate = strongSelf; - [pack requestProgress]; - } - - if (error) { - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Can’t Find Offline Packs" message:@"Mapbox GL was unable to find the existing offline packs." preferredStyle:UIAlertControllerStyleAlert]; - [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - [self presentViewController:alertController animated:YES completion:^{ - [strongSelf dismissViewControllerAnimated:YES completion:nil]; + [[MGLOfflineStorage sharedOfflineStorage] addObserver:self forKeyPath:@"packs" options:NSKeyValueObservingOptionInitial context:NULL]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackProgressDidChange:) name:MGLOfflinePackProgressChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveError:) name:MGLOfflinePackErrorNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveMaximumAllowedMapboxTiles:) name:MGLOfflinePackMaximumMapboxTilesReachedNotification object:nil]; +} + +- (void)dealloc { + [[MGLOfflineStorage sharedOfflineStorage] removeObserver:self forKeyPath:@"packs"]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NS_DICTIONARY_OF(NSString *, id) *)change context:(void *)context { + if ([keyPath isEqualToString:@"packs"]) { + NSKeyValueChange changeKind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue]; + NSIndexSet *indices = change[NSKeyValueChangeIndexesKey]; + NSMutableArray *indexPaths; + if (indices) { + indexPaths = [NSMutableArray arrayWithCapacity:indices.count]; + [indices enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + [indexPaths addObject:[NSIndexPath indexPathForRow:idx inSection:0]]; }]; } - }]; + switch (changeKind) { + case NSKeyValueChangeInsertion: + [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic]; + break; + + case NSKeyValueChangeRemoval: + [self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic]; + break; + + case NSKeyValueChangeReplacement: + [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic]; + break; + + default: + [self.tableView reloadData]; + break; + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } } - (IBAction)addCurrentRegion:(id)sender { @@ -89,13 +106,6 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Can’t Add Offline Pack" message:message preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alertController animated:YES completion:nil]; - } else { - pack.delegate = strongSelf; - [pack resume]; - - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:strongSelf.offlinePacks.count inSection:0]; - [strongSelf.offlinePacks addObject:pack]; - [strongSelf.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } }]; }]; @@ -110,11 +120,11 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac #pragma mark - Table view data source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return self.offlinePacks.count; + return [MGLOfflineStorage sharedOfflineStorage].packs.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - MGLOfflinePack *pack = self.offlinePacks[indexPath.row]; + MGLOfflinePack *pack = [MGLOfflineStorage sharedOfflineStorage].packs[indexPath.row]; NSString *reuseIdentifier = pack.state == MGLOfflinePackStateActive ? MBXOfflinePacksTableViewActiveCellReuseIdentifier : MBXOfflinePacksTableViewInactiveCellReuseIdentifier; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath]; @@ -165,9 +175,7 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { - MGLOfflinePack *pack = self.offlinePacks[indexPath.row]; - [self.offlinePacks removeObjectAtIndex:indexPath.row]; - [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + MGLOfflinePack *pack = [MGLOfflineStorage sharedOfflineStorage].packs[indexPath.row]; [[MGLOfflineStorage sharedOfflineStorage] removePack:pack withCompletionHandler:nil]; } } @@ -177,7 +185,7 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; - MGLOfflinePack *pack = self.offlinePacks[indexPath.row]; + MGLOfflinePack *pack = [MGLOfflineStorage sharedOfflineStorage].packs[indexPath.row]; switch (pack.state) { case MGLOfflinePackStateUnknown: break; @@ -207,8 +215,11 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac #pragma mark - Offline pack delegate -- (void)offlinePack:(MGLOfflinePack *)pack progressDidChange:(MGLOfflinePackProgress)progress { - NSUInteger index = [self.offlinePacks indexOfObject:pack]; +- (void)offlinePackProgressDidChange:(NSNotification *)notification { + MGLOfflinePack *pack = notification.object; + NSAssert([pack isKindOfClass:[MGLOfflinePack class]], @"MGLOfflineStorage notification has a non-pack object."); + + NSUInteger index = [[MGLOfflineStorage sharedOfflineStorage].packs indexOfObject:pack]; if (index == NSNotFound) { return; } @@ -217,11 +228,28 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; } -- (void)offlinePack:(MGLOfflinePack *)pack didReceiveError:(NSError *)error { - NSLog(@"Offline pack “%@” received error: %@", pack.name, error.localizedFailureReason); +- (void)offlinePackDidReceiveError:(NSNotification *)notification { + MGLOfflinePack *pack = notification.object; + NSAssert([pack isKindOfClass:[MGLOfflinePack class]], @"MGLOfflineStorage notification has a non-pack object."); + + NSError *error = notification.userInfo[MGLOfflinePackErrorUserInfoKey]; + NSAssert([error isKindOfClass:[NSError class]], @"MGLOfflineStorage notification has a non-error error."); + + NSString *message = [NSString stringWithFormat:@"iosapp encountered an error while downloading the offline pack “%@”: %@", pack.name, error.localizedFailureReason]; + if (error.code == MGLErrorCodeConnectionFailed) { + NSLog(@"%@", message); + } else { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Error Downloading Offline Pack" message:message preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alertController animated:YES completion:nil]; + } } -- (void)offlinePack:(MGLOfflinePack *)pack didReceiveMaximumAllowedMapboxTiles:(uint64_t)maximumCount { +- (void)offlinePackDidReceiveMaximumAllowedMapboxTiles:(NSNotification *)notification { + MGLOfflinePack *pack = notification.object; + NSAssert([pack isKindOfClass:[MGLOfflinePack class]], @"MGLOfflineStorage notification has a non-pack object."); + + uint64_t maximumCount = [notification.userInfo[MGLOfflinePackMaximumCountUserInfoKey] unsignedLongLongValue]; NSLog(@"Offline pack “%@” reached limit of %llu tiles.", pack.name, maximumCount); }