From 9d8839c4b83e2ea7901e0f06ba1985cd7547f01b Mon Sep 17 00:00:00 2001 From: Jordan Kiley Date: Tue, 16 Jul 2019 14:58:32 -0700 Subject: [ios] Cache Management API (#14978) --- platform/darwin/src/MGLOfflineStorage.h | 87 ++++++++++++++++- platform/darwin/src/MGLOfflineStorage.mm | 106 +++++++++++++++++++-- platform/darwin/src/MGLTypes.h | 4 +- platform/darwin/test/MGLOfflineStorageTests.mm | 62 ++++++++++++ .../ios/app/MBXOfflinePacksTableViewController.m | 13 +++ platform/ios/app/Main.storyboard | 53 ++++++----- 6 files changed, 288 insertions(+), 37 deletions(-) diff --git a/platform/darwin/src/MGLOfflineStorage.h b/platform/darwin/src/MGLOfflineStorage.h index b48f2ebf2c..c6a51df80c 100644 --- a/platform/darwin/src/MGLOfflineStorage.h +++ b/platform/darwin/src/MGLOfflineStorage.h @@ -171,9 +171,9 @@ typedef NS_ENUM(NSUInteger, MGLResourceKind) { /** MGLOfflineStorage implements a singleton (shared object) that manages offline - packs. All of this class’s instance methods are asynchronous, reflecting the - fact that offline resources are stored in a database. The shared object - maintains a canonical collection of offline packs in its `packs` property. + packs and ambient caching. All of this class’s instance methods are asynchronous, + reflecting the fact that offline resources are stored in a database. The shared + object maintains a canonical collection of offline packs in its `packs` property. #### Related examples See the @@ -303,6 +303,21 @@ MGL_EXPORT */ - (void)removePack:(MGLOfflinePack *)pack withCompletionHandler:(nullable MGLOfflinePackRemovalCompletionHandler)completion; +/** + Invalidates the specified offline pack. This method checks that the tiles + in the specified offline pack match those from the server. Local tiles that + do not match the latest version on the server are updated. + + This is more efficient than deleting the offline pack and downloading it + again. If the data stored locally matches that on the server, new data will + not be downloaded. + + @param pack The offline pack to be invalidated. + @param completion The completion handler to call once the pack has been + removed. This handler is executed asynchronously on the main queue. + */ + +- (void)invalidatePack:(MGLOfflinePack *)pack withCompletionHandler:(void (^)(NSError * _Nullable))completion; /** Forcibly, asynchronously reloads the `packs` property. At some point after this method is called, the pointer values of the `MGLOfflinePack` objects in the @@ -341,6 +356,72 @@ MGL_EXPORT */ @property (nonatomic, readonly) unsigned long long countOfBytesCompleted; + +#pragma mark - Managing Ambient Cache + +/** + Sets the maximum ambient cache size in megabytes. The default maximum cache + size is 50 MB. To disable ambient caching, set the maximum ambient cache size + to `0`. Setting the maximum ambient cache size does not impact the maximum size + of offline packs. + + While this method does not limit the space available to offline packs, + data in offline packs count towards this limit. If the maximum ambient + cache size is set to 30 MB and 20 MB of offline packs are downloaded, + there may be only 10 MB reserved for the ambient cache. + + This method should be called before the map and map style have been loaded. + + This method is potentially expensive, as the database will trim cached data + in order to prevent the ambient cache from being larger than the + specified amount. + + @param cacheSize The maximum size in bytes for the ambient cache. + @param completion The completion handler to call once the maximum ambient cache size + has been set. This handler is executed synchronously on the main queue. + */ + +- (void)setMaximumAmbientCacheSize:(NSUInteger)cacheSize withCompletionHandler:(void (^)(NSError *_Nullable error))completion; + +/** + Invalidates the ambient cache. This method checks that the tiles in the + ambient cache match those from the server. If the local tiles do not match + those on the server, they are re-downloaded. + + This is recommended over clearing the cache or resetting the database + because valid local tiles will not be downloaded again. + + Resources shared with offline packs will not be affected by this method. + + @param completion The completion handler to call once the ambient cache has + been revalidated. This handler is executed asynchronously on the main queue. + */ + +- (void)invalidateAmbientCacheWithCompletionHandler:(void (^)(NSError *_Nullable error))completion; + +/** + Clears the ambient cache by deleting resources. This method does not + affect resources shared with offline regions. + + @param completion The completion handler to call once resources from + the ambient cache have been cleared. This handler is executed + asynchronously on the main queue. + */ + +- (void)clearAmbientCacheWithCompletionHandler:(void (^)(NSError *_Nullable error))completion; + +/** + Deletes the existing database, which includes both the ambient cache and offline packs, + then reinitializes it. + + You typically do not need to call this method. + + @param completion The completion handler to call once the pack has database has + been reset. This handler is executed asynchronously on the main queue. + */ + +- (void)resetDatabaseWithCompletionHandler:(void (^)(NSError *_Nullable error))completion; + /* Inserts the provided resource into the ambient cache. diff --git a/platform/darwin/src/MGLOfflineStorage.mm b/platform/darwin/src/MGLOfflineStorage.mm index 6effd8c3ce..93f986a518 100644 --- a/platform/darwin/src/MGLOfflineStorage.mm +++ b/platform/darwin/src/MGLOfflineStorage.mm @@ -48,7 +48,6 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio @property (nonatomic) std::shared_ptr mbglFileSource; @property (nonatomic) std::string mbglCachePath; @property (nonatomic, getter=isPaused) BOOL paused; - @end @implementation MGLOfflineStorage { @@ -341,10 +340,11 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio NSMutableArray *packs; if (!result) { NSString *description = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ADD_FILE_CONTENTS_FAILED_DESC", @"Foundation", nil, @"Unable to add offline packs from the file at %@.", @"User-friendly error description"), filePath]; - error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{ - NSLocalizedDescriptionKey: description, - NSLocalizedFailureReasonErrorKey: @(mbgl::util::toString(result.error()).c_str()) - }]; + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed + userInfo:@{ + NSLocalizedDescriptionKey: description, + NSLocalizedFailureReasonErrorKey: @(mbgl::util::toString(result.error()).c_str()) + }]; } else { auto& regions = result.value(); packs = [NSMutableArray arrayWithCapacity:regions.size()]; @@ -401,7 +401,7 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio NSError *error; if (!mbglOfflineRegion) { NSString *errorDescription = @(mbgl::util::toString(mbglOfflineRegion.error()).c_str()); - error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:errorDescription ? @{ + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:errorDescription ? @{ NSLocalizedDescriptionKey: errorDescription, } : nil]; } @@ -431,11 +431,10 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio completion(nil); return; } - _mbglFileSource->deleteOfflineRegion(std::move(*mbglOfflineRegion), [&, completion](std::exception_ptr exception) { NSError *error; if (exception) { - error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{ + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{ NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()), }]; } @@ -445,6 +444,29 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio }); } }); + +} + +- (void)invalidatePack:(MGLOfflinePack *)pack withCompletionHandler:(void (^)(NSError * _Nullable))completion { + mbgl::OfflineRegion& region = *pack.mbglOfflineRegion; + NSError *error; + if (!pack.mbglOfflineRegion) { + completion(nil); + return; + } + + _mbglFileSource->invalidateOfflineRegion(region, [&](std::exception_ptr exception) { + if (exception) { + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{ + NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()), + }]; + } + }); + if (completion) { + dispatch_async(dispatch_get_main_queue(), [&, completion, error](void) { + completion(error); + }); + } } - (void)reloadPacks { @@ -462,7 +484,7 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio NSError *error; NSMutableArray *packs; if (!result) { - error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{ + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeUnknown userInfo:@{ NSLocalizedDescriptionKey: @(mbgl::util::toString(result.error()).c_str()), }]; } else { @@ -486,6 +508,72 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio _mbglFileSource->setOfflineMapboxTileCountLimit(maximumCount); } +#pragma mark - Ambient Cache management + +- (void)setMaximumAmbientCacheSize:(NSUInteger)cacheSize withCompletionHandler:(void (^)(NSError * _Nullable))completion { + _mbglFileSource->setMaximumAmbientCacheSize(cacheSize, [&, completion](std::exception_ptr exception) { + NSError *error; + if (completion) { + if (exception) { + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{ + NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()), + }]; + } + dispatch_sync(dispatch_get_main_queue(), ^ { + completion(error); + }); + } + }); +} + +- (void)invalidateAmbientCacheWithCompletionHandler:(void (^)(NSError *_Nullable))completion { + _mbglFileSource->invalidateAmbientCache([&, completion](std::exception_ptr exception){ + NSError *error; + if (completion) { + if (exception) { + // Convert std::exception_ptr to an NSError. + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{ + NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()), + }]; + } + dispatch_async(dispatch_get_main_queue(), ^ { + completion(error); + }); + } + }); +} + +- (void)clearAmbientCacheWithCompletionHandler:(void (^)(NSError *_Nullable error))completion { + _mbglFileSource->clearAmbientCache([&, completion](std::exception_ptr exception){ + NSError *error; + if (completion) { + if (exception) { + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeModifyingOfflineStorageFailed userInfo:@{ + NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()), + }]; + } + dispatch_async(dispatch_get_main_queue(), [&, completion, error](void) { + completion(error); + }); + } + }); +} + +- (void)resetDatabaseWithCompletionHandler:(void (^)(NSError *_Nullable error))completion { + _mbglFileSource->resetDatabase([&, completion](std::exception_ptr exception) { + NSError *error; + if (completion) { + if (exception) { + error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeUnknown userInfo:@{ + NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()), + }]; + } + dispatch_async(dispatch_get_main_queue(), ^{ + completion(error); + }); + } + }); +} #pragma mark - - (unsigned long long)countOfBytesCompleted { diff --git a/platform/darwin/src/MGLTypes.h b/platform/darwin/src/MGLTypes.h index b8354d2e83..7e0dd27141 100644 --- a/platform/darwin/src/MGLTypes.h +++ b/platform/darwin/src/MGLTypes.h @@ -53,7 +53,9 @@ typedef NS_ENUM(NSInteger, MGLErrorCode) { /** Source is in use and cannot be removed */ MGLErrorCodeSourceIsInUseCannotRemove = 7, /** Source is in use and cannot be removed */ - MGLErrorCodeSourceIdentifierMismatch = 8 + MGLErrorCodeSourceIdentifierMismatch = 8, + /** An error occurred while modifying the offline storage database */ + MGLErrorCodeModifyingOfflineStorageFailed = 9 }; /** Options for enabling debugging features in an `MGLMapView` instance. */ diff --git a/platform/darwin/test/MGLOfflineStorageTests.mm b/platform/darwin/test/MGLOfflineStorageTests.mm index 5551d8889b..ee4bcc2c65 100644 --- a/platform/darwin/test/MGLOfflineStorageTests.mm +++ b/platform/darwin/test/MGLOfflineStorageTests.mm @@ -201,6 +201,68 @@ pack = nil; } +- (void)testInvalidatePack { + XCTestExpectation *expectation = [self expectationWithDescription:@"Expect offline pack to be invalidated without an error."]; + MGLCoordinateBounds bounds = { + { .latitude = 48.8660, .longitude = 2.3306 }, + { .latitude = 48.8603, .longitude = 2.3213 }, + }; + + NSURL *styleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"one-liner" withExtension:@"json"]; + MGLTilePyramidOfflineRegion *region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL bounds:bounds fromZoomLevel:10 toZoomLevel:11]; + + NSString *nameKey = @"Name"; + NSString *name = @"Paris square"; + + NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{nameKey: name}]; + [[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack * _Nullable pack, NSError * _Nullable error) { + XCTAssertNotNil(pack); + [[MGLOfflineStorage sharedOfflineStorage] invalidatePack:pack withCompletionHandler:^(NSError * _Nullable) { + XCTAssertNotNil(pack); + XCTAssertNil(error); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testSetMaximumAmbientCache { + XCTestExpectation *expectation = [self expectationWithDescription:@"Expect maximum cache size to be raised without an error."]; + [[MGLOfflineStorage sharedOfflineStorage] setMaximumAmbientCacheSize:0 withCompletionHandler:^(NSError * _Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testInvalidateAmbientCache { + XCTestExpectation *expectation = [self expectationWithDescription:@"Expect cache to be invalidated without an error."]; + [[MGLOfflineStorage sharedOfflineStorage] invalidateAmbientCacheWithCompletionHandler:^(NSError * _Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testClearCache { + XCTestExpectation *expectation = [self expectationWithDescription:@"Expect cache to be cleared without an error."]; + [[MGLOfflineStorage sharedOfflineStorage] clearAmbientCacheWithCompletionHandler:^(NSError * _Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testResetDatabase { + XCTestExpectation *expectation = [self expectationWithDescription:@"Expect database to be reset without an error."]; + [[MGLOfflineStorage sharedOfflineStorage] resetDatabaseWithCompletionHandler:^(NSError * _Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + - (void)testBackupExclusion { NSURL *cacheDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask diff --git a/platform/ios/app/MBXOfflinePacksTableViewController.m b/platform/ios/app/MBXOfflinePacksTableViewController.m index 4459711b58..90497ca939 100644 --- a/platform/ios/app/MBXOfflinePacksTableViewController.m +++ b/platform/ios/app/MBXOfflinePacksTableViewController.m @@ -128,6 +128,19 @@ static NSString * const MBXOfflinePacksTableViewActiveCellReuseIdentifier = @"Ac [self presentViewController:alertController animated:YES completion:nil]; } +- (IBAction)invalidatePacks:(id)sender { + for (MGLOfflinePack *pack in [MGLOfflineStorage sharedOfflineStorage].packs) { + + CFTimeInterval start = CACurrentMediaTime(); + [[MGLOfflineStorage sharedOfflineStorage] invalidatePack:pack withCompletionHandler:^(NSError * _Nullable error) { + CFTimeInterval end = CACurrentMediaTime(); + CFTimeInterval difference = end - start; + NSLog(@"invalidatePack Started: %f Ended: %f Total Time: %f", start, end, difference); + }]; + } +} + + #pragma mark - Table view data source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { diff --git a/platform/ios/app/Main.storyboard b/platform/ios/app/Main.storyboard index f4e535a56c..28316745a1 100644 --- a/platform/ios/app/Main.storyboard +++ b/platform/ios/app/Main.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -15,14 +13,14 @@ - + - +