diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index afe113f91a..f4fb6af296 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -234,7 +234,7 @@ typedef void (^BITLatestImageFetchCompletionBlock)(UIImage *_Nonnull latestImage #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" - NSArray *preparedItems = self.feedbackComposerPreparedItems; + NSArray *preparedItems = self.feedbackComposerPreparedItems ?: [NSArray array]; #pragma clang diagnostic pop if ([self.delegate respondsToSelector:@selector(preparedItemsForFeedbackManager:)]) { preparedItems = [preparedItems arrayByAddingObjectsFromArray:[self.delegate preparedItemsForFeedbackManager:self]]; diff --git a/Classes/BITHockeyHelper.h b/Classes/BITHockeyHelper.h index ebc8e9cbd3..2250623ac6 100644 --- a/Classes/BITHockeyHelper.h +++ b/Classes/BITHockeyHelper.h @@ -32,6 +32,8 @@ @interface BITHockeyHelper : NSObject +FOUNDATION_EXPORT NSString *const kBITExcludeApplicationSupportFromBackup; + + (BOOL)isURLSessionSupported; @end @@ -41,6 +43,9 @@ NSString *bit_settingsDir(void); BOOL bit_validateEmail(NSString *email); NSString *bit_keychainHockeySDKServiceName(void); +/* Fix bug where Application Support was excluded from backup. */ +void bit_fixBackupAttributeForURL(NSURL *directoryURL); + NSComparisonResult bit_versionCompare(NSString *stringA, NSString *stringB); NSString *bit_mainBundleIdentifier(void); NSString *bit_encodeAppIdentifier(NSString *inputString); @@ -97,4 +102,5 @@ UIImage *bit_imageWithContentsOfResolutionIndependentFile(NSString * path); UIImage *bit_imageNamed(NSString *imageName, NSString *bundleName); UIImage *bit_screenshot(void); UIImage *bit_appIcon(void); + #endif diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 7e65e6de21..4fbedbd643 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -38,6 +38,7 @@ #import static NSString *const kBITUtcDateFormatter = @"utcDateFormatter"; +NSString *const kBITExcludeApplicationSupportFromBackup = @"kBITExcludeApplicationSupportFromBackup"; @implementation BITHockeyHelper @@ -141,6 +142,28 @@ NSComparisonResult bit_versionCompare(NSString *stringA, NSString *stringB) { return result; } +#pragma mark Exclude from backup fix + +void bit_fixBackupAttributeForURL(NSURL *directoryURL) { + + BOOL shouldExcludeAppSupportDirFromBackup = [[NSUserDefaults standardUserDefaults] boolForKey:kBITExcludeApplicationSupportFromBackup]; + if (shouldExcludeAppSupportDirFromBackup) { + return; + } + + if (directoryURL) { + NSError *getResourceError = nil; + NSNumber *appSupportDirExcludedValue; + + if ([directoryURL getResourceValue:&appSupportDirExcludedValue forKey:NSURLIsExcludedFromBackupKey error:&getResourceError] && appSupportDirExcludedValue) { + NSError *setResourceError = nil; + [directoryURL setResourceValue:@NO forKey:NSURLIsExcludedFromBackupKey error:&setResourceError]; + } + } +} + +#pragma mark Identifiers + NSString *bit_mainBundleIdentifier(void) { return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; } @@ -246,6 +269,8 @@ NSString *bit_appAnonID(BOOL forceNewAnonID) { return appAnonID; } +#pragma mark Environment detection + BOOL bit_isPreiOS7Environment(void) { static BOOL isPreiOS7Environment = YES; static dispatch_once_t checkOS; diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index 78008705f9..d058486c37 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -228,6 +228,11 @@ bitstadium_info_t bitstadium_library_info __attribute__((section("__TEXT,__bit_h return; } + // Fix bug where Application Support directory was encluded from backup + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *appSupportURL = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject]; + bit_fixBackupAttributeForURL(appSupportURL); + if (![self isSetUpOnMainThread]) return; if ((self.appEnvironment == BITEnvironmentAppStore) && [self isInstallTrackingDisabled]) { diff --git a/Classes/BITPersistence.m b/Classes/BITPersistence.m index 9b99fe523b..d86cdfaa7e 100644 --- a/Classes/BITPersistence.m +++ b/Classes/BITPersistence.m @@ -13,8 +13,10 @@ static NSString *const kBITTelemetry = @"Telemetry"; static NSString *const kBITMetaData = @"MetaData"; static NSString *const kBITFileBaseString = @"hockey-app-bundle-"; static NSString *const kBITFileBaseStringMeta = @"metadata"; -static NSString *const kBITTelemetryDirectoryPath = @"com.microsoft.HockeyApp/Telemetry/"; -static NSString *const kBITMetaDataDirectoryPath = @"com.microsoft.HockeyApp/MetaData/"; + +static NSString *const kBITHockeyDirectory = @"com.microsoft.HockeyApp"; +static NSString *const kBITTelemetryDirectory = @"Telemetry"; +static NSString *const kBITMetaDataDirectory = @"MetaData"; static char const *kBITPersistenceQueueString = "com.microsoft.HockeyApp.persistenceQueue"; static NSUInteger const BITDefaultFileCount = 50; @@ -158,8 +160,6 @@ static NSUInteger const BITDefaultFileCount = 50; #pragma mark - Private - (NSString *)fileURLForType:(BITPersistenceType)type { - NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); - NSString *appSupportPath = searchPaths.lastObject; NSString *fileName = nil; NSString *filePath; @@ -167,13 +167,13 @@ static NSUInteger const BITDefaultFileCount = 50; switch (type) { case BITPersistenceTypeMetaData: { fileName = kBITFileBaseStringMeta; - filePath = [appSupportPath stringByAppendingPathComponent:kBITMetaDataDirectoryPath]; + filePath = [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:kBITMetaDataDirectory]; break; }; default: { NSString *uuid = bit_UUID(); fileName = [NSString stringWithFormat:@"%@%@", kBITFileBaseString, uuid]; - filePath = [appSupportPath stringByAppendingPathComponent:kBITTelemetryDirectoryPath]; + filePath = [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:kBITTelemetryDirectory]; break; }; } @@ -187,39 +187,46 @@ static NSUInteger const BITDefaultFileCount = 50; * Create directory structure if necessary and exclude it from iCloud backup */ - (void)createDirectoryStructureIfNeeded { - //Application Support Dir + + NSURL *appURL = [NSURL fileURLWithPath:self.appHockeySDKDirectoryPath]; NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *appSupportURL = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject]; - if (appSupportURL) { + if (appURL) { NSError *error = nil; - //App Support and Telemetry Directory - NSURL *folderURL = [appSupportURL URLByAppendingPathComponent:kBITTelemetryDirectoryPath]; + + // Create HockeySDK folder if needed + if (![fileManager createDirectoryAtURL:appURL withIntermediateDirectories:YES attributes:nil error:&error]) { + BITHockeyLogError(@"ERROR: %@", error.localizedDescription); + return; + } + + // Create metadata subfolder + NSURL *metaDataURL = [appURL URLByAppendingPathComponent:kBITMetaDataDirectory]; + if (![fileManager createDirectoryAtURL:metaDataURL withIntermediateDirectories:YES attributes:nil error:&error]) { + BITHockeyLogError(@"ERROR: %@", error.localizedDescription); + return; + } + + // Create telemetry subfolder + //NOTE: createDirectoryAtURL:withIntermediateDirectories:attributes:error //will return YES if the directory already exists and won't override anything. //No need to check if the directory already exists. - if (![fileManager createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:&error]) { + NSURL *telemetryURL = [appURL URLByAppendingPathComponent:kBITTelemetryDirectory]; + if (![fileManager createDirectoryAtURL:telemetryURL withIntermediateDirectories:YES attributes:nil error:&error]) { BITHockeyLogError(@"ERROR: %@", error.localizedDescription); - return; //TODO we can't use persistence at all in this case, what do we want to do now? Notify the user? + return; } - - //MetaData Directory - folderURL = [appSupportURL URLByAppendingPathComponent:kBITMetaDataDirectoryPath]; - if (![fileManager createDirectoryAtURL:folderURL withIntermediateDirectories:NO attributes:nil error:&error]) { - BITHockeyLogError(@"ERROR: %@", error.localizedDescription); - return; //TODO we can't use persistence at all in this case, what do we want to do now? Notify the user? + + //Exclude HockeySDK folder from backup + if (![appURL setResourceValue:@YES + forKey:NSURLIsExcludedFromBackupKey + error:&error]) { + BITHockeyLogError(@"ERROR: Error excluding %@ from backup %@", appURL.lastPathComponent, error.localizedDescription); + } else { + BITHockeyLogDebug(@"INFO: Exclude %@ from backup", appURL); } - + _directorySetupComplete = YES; - - //Exclude from Backup - if (![appSupportURL setResourceValue:@YES - forKey:NSURLIsExcludedFromBackupKey - error:&error]) { - BITHockeyLogError(@"Error excluding %@ from backup %@", appSupportURL.lastPathComponent, error.localizedDescription); - } - else { - BITHockeyLogDebug(@"INFO: Excluding %@ from backup", appSupportURL); - } } } @@ -250,21 +257,18 @@ static NSUInteger const BITDefaultFileCount = 50; } - (NSString *)folderPathForType:(BITPersistenceType)type { - NSString *path = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject]; NSString *subFolder = @""; switch (type) { case BITPersistenceTypeTelemetry: { - subFolder = kBITTelemetryDirectoryPath; + subFolder = kBITTelemetryDirectory; break; } case BITPersistenceTypeMetaData: { - subFolder = kBITMetaDataDirectoryPath; + subFolder = kBITMetaDataDirectory; break; } } - path = [path stringByAppendingPathComponent:subFolder]; - - return path; + return [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:subFolder]; } /** @@ -279,6 +283,16 @@ static NSUInteger const BITDefaultFileCount = 50; }); } +- (NSString *)appHockeySDKDirectoryPath { + if (!_appHockeySDKDirectoryPath) { + NSString *appSupportPath = [[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject] stringByStandardizingPath]; + if (appSupportPath) { + _appHockeySDKDirectoryPath = [appSupportPath stringByAppendingPathComponent:kBITHockeyDirectory]; + } + } + return _appHockeySDKDirectoryPath; +} + @end #endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/Classes/BITPersistencePrivate.h b/Classes/BITPersistencePrivate.h index 3d494c0e19..6aa342d5cd 100644 --- a/Classes/BITPersistencePrivate.h +++ b/Classes/BITPersistencePrivate.h @@ -37,6 +37,8 @@ FOUNDATION_EXPORT NSString *const BITPersistenceSuccessNotification; */ @property (nonatomic, assign) NSUInteger maxFileCount; +@property (nonatomic, strong) NSString *appHockeySDKDirectoryPath; + /** * An array with all file paths, that have been requested by the sender. If the sender * triggers a delete, the appropriate path should also be removed here. We keep to @@ -134,6 +136,7 @@ FOUNDATION_EXPORT NSString *const BITPersistenceSuccessNotification; */ - (NSString *)fileURLForType:(BITPersistenceType)type; +- (void)createDirectoryStructureIfNeeded; #endif /* HOCKEYSDK_FEATURE_METRICS */ diff --git a/README.md b/README.md index 45d2d2966a..f16f2838a4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ - [Changelog](http://www.hockeyapp.net/help/sdk/ios/4.1.0-beta.1/docs/docs/Changelog.html) +NOTE: With the release of HockeySDK 4.0.0-alpha.1 a bug was introduced which lead to the exclusion of the Application Support folder from iCloud and iTunes backups. + +If you have been using one of the affected versions (4.0.0-alpha.2, Version 4.0.0-beta.1, 4.0.0, 4.1.0-alpha.1, 4.1.0-alpha.2, or Version 4.1.0-beta.1), please make sure to update to at least version 4.0.1 or 4.1.0-beta.2 of our SDK as soon as you can. + ## Introduction HockeySDK-iOS implements support for using HockeyApp in your iOS applications. diff --git a/Support/HockeySDKTests/BITFeedbackManagerTests.m b/Support/HockeySDKTests/BITFeedbackManagerTests.m index 7a1dc309f3..4f5ac3d302 100644 --- a/Support/HockeySDKTests/BITFeedbackManagerTests.m +++ b/Support/HockeySDKTests/BITFeedbackManagerTests.m @@ -236,24 +236,35 @@ self.sut.feedbackComposeHideImageAttachmentButton = YES; XCTAssertTrue(self.sut.feedbackComposeHideImageAttachmentButton); - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated" - self.sut.feedbackComposerPreparedItems = @[sampleImage1, sampleData1]; -#pragma clang diagnostic pop id mockDelegate = mockProtocol(@protocol(BITFeedbackManagerDelegate)); [given([mockDelegate preparedItemsForFeedbackManager:self.sut]) willReturn:@[sampleImage2, sampleData2]]; self.sut.delegate = mockDelegate; + + // Test when feedbackComposerPreparedItems is also set +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + self.sut.feedbackComposerPreparedItems = @[sampleImage1, sampleData1]; +#pragma clang diagnostic pop + BITFeedbackComposeViewController *composeViewController = [self.sut feedbackComposeViewController]; - NSArray *attachments = [composeViewController performSelector:@selector(attachments)]; - XCTAssertEqual(attachments.count, 4); + + + // Test when feedbackComposerPreparedItems is nil +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + self.sut.feedbackComposerPreparedItems = nil; +#pragma clang diagnostic pop + + composeViewController = [self.sut feedbackComposeViewController]; + attachments = [composeViewController performSelector:@selector(attachments)]; + XCTAssertEqual(attachments.count, 2); + XCTAssertTrue(composeViewController.hideImageAttachmentButton); - XCTAssertEqual(composeViewController.delegate, mockDelegate); } diff --git a/Support/HockeySDKTests/BITHockeyHelperTests.m b/Support/HockeySDKTests/BITHockeyHelperTests.m index 5f0cdc30cd..62883f098f 100644 --- a/Support/HockeySDKTests/BITHockeyHelperTests.m +++ b/Support/HockeySDKTests/BITHockeyHelperTests.m @@ -31,6 +31,7 @@ - (void)tearDown { // Tear-down code here. [super tearDown]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kBITExcludeApplicationSupportFromBackup]; } - (void)testURLEncodedString { @@ -229,4 +230,62 @@ assertThat(result, nilValue()); } +- (void)testBackupFixRemovesExcludeAttribute { + + // Setup: Attribute is set and NSUSerDefaults DON'T contain kBITExcludeApplicationSupportFromBackup == YES + NSURL *testAppSupportURL = [self createBackupExcludedTestDirectoryForURL]; + XCTAssertNotNil(testAppSupportURL); + XCTAssertTrue([self excludeAttributeIsSetForURL:testAppSupportURL]); + + // Test + bit_fixBackupAttributeForURL(testAppSupportURL); + + // Verify + XCTAssertFalse([self excludeAttributeIsSetForURL:testAppSupportURL]); +} + +- (void)testBackupFixIgnoresRemovalOfExcludeAttributeUserDefaultsContainKey { + + // Setup: Attribute is set and NSUSerDefaults DO contain kBITExcludeApplicationSupportFromBackup == YES + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITExcludeApplicationSupportFromBackup]; + NSURL *testAppSupportURL = [self createBackupExcludedTestDirectoryForURL]; + XCTAssertNotNil(testAppSupportURL); + XCTAssertTrue([self excludeAttributeIsSetForURL:testAppSupportURL]); + + // Test + bit_fixBackupAttributeForURL(testAppSupportURL); + + // Verify + XCTAssertTrue([self excludeAttributeIsSetForURL:testAppSupportURL]); +} + +#pragma mark - Test Helper + +- (NSURL *)createBackupExcludedTestDirectoryForURL{ + NSString *testDirectory = @"HockeyTest"; + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSURL *testAppSupportURL = [[[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:testDirectory]; + if ([fileManager createDirectoryAtURL:testAppSupportURL withIntermediateDirectories:YES attributes:nil error:nil]) { + if ([testAppSupportURL setResourceValue:@YES + forKey:NSURLIsExcludedFromBackupKey + error:nil]) { + return testAppSupportURL; + } + } + return nil; +} + +- (BOOL)excludeAttributeIsSetForURL:(NSURL *)directoryURL { + + NSError *getResourceError = nil; + NSNumber *appSupportDirExcludedValue; + if ([directoryURL getResourceValue:&appSupportDirExcludedValue forKey:NSURLIsExcludedFromBackupKey error:&getResourceError] && appSupportDirExcludedValue) { + if ([appSupportDirExcludedValue isEqualToValue:@YES]) { + return YES; + } + } + return NO; +} + @end diff --git a/Support/HockeySDKTests/BITPersistenceTests.m b/Support/HockeySDKTests/BITPersistenceTests.m index 753290a59e..c06dc93a86 100644 --- a/Support/HockeySDKTests/BITPersistenceTests.m +++ b/Support/HockeySDKTests/BITPersistenceTests.m @@ -45,6 +45,55 @@ [self.mockNotificationCenter removeObserver:observerMock]; } +- (void)testCreateDirectoryStructureIfNeeded { + // Setup + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *appSupportPath = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject]; + NSString *path = self.sut.appHockeySDKDirectoryPath; + + NSError *fileRemovalError = nil; + [fileManager removeItemAtPath:path error:&fileRemovalError]; + + NSError *getResourceError = nil; + NSNumber *resourveValue = nil; + XCTAssertTrue([[NSURL fileURLWithPath:appSupportPath] setResourceValue:@NO + forKey:NSURLIsExcludedFromBackupKey + error:&getResourceError]); + + // Assert + XCTAssertNil(fileRemovalError); + XCTAssertFalse([fileManager fileExistsAtPath:path]); + + // Act + [self.sut createDirectoryStructureIfNeeded]; + + // Verify + BOOL isDirectory = NO; + [fileManager fileExistsAtPath:path isDirectory:&isDirectory]; + + XCTAssertTrue(isDirectory); + + // Flag stays @NO on Application Support directory + getResourceError = nil; + resourveValue = nil; + [[NSURL fileURLWithPath:appSupportPath] getResourceValue:&resourveValue + forKey:NSURLIsExcludedFromBackupKey + error:&getResourceError]; + XCTAssertNil(getResourceError); + XCTAssertEqual(resourveValue, @NO); + + // Flag is set to @YES on our custom subdirectory + getResourceError = nil; + resourveValue = nil; + [[NSURL fileURLWithPath:path] getResourceValue:&resourveValue + forKey:NSURLIsExcludedFromBackupKey + error:&getResourceError]; + XCTAssertNil(getResourceError); + XCTAssertEqual(resourveValue, @YES); + + // TODO: Check subdirectories have been created +} + - (void)testFolderPathForType { NSString *path = [self.sut folderPathForType:BITPersistenceTypeTelemetry]; XCTAssertFalse([path rangeOfString:@"com.microsoft.HockeyApp/Telemetry"].location == NSNotFound); diff --git a/docs/Changelog-template.md b/docs/Changelog-template.md index 497228bd66..c4b08ac3de 100644 --- a/docs/Changelog-template.md +++ b/docs/Changelog-template.md @@ -24,6 +24,23 @@ - [IMPROVEMENT] Reuse `NSURLSession` object - [IMPROVEMENT] Under the hood improvements and cleanup +## Version 4.0.1 + +- [BUGFIX] Fixes an issue where the whole app's Application Support directory was accidentally excluded from backups. +This SDK release explicitly includes the Application Support directory into backups. If you want to opt-out of this fix and keep the Application Directory's backup flag untouched, add the following line above the SDK setup code: + + - Objective-C: + ```objectivec + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"BITExcludeApplicationSupportFromBackup"]; + ``` + + - Swift: + ```swift + NSUserDefaults.standardUserDefaults().setBool(true, forKey: "BITExcludeApplicationSupportFromBackup") + ``` + +- [BUGFIX] Fixes an issue that prevented preparedItemsForFeedbackManager: delegate method from working + ## Version 4.0.0 - [NEW] Added official Carthage support diff --git a/docs/Guide-Installation-Setup-template.md b/docs/Guide-Installation-Setup-template.md index 716003163c..e9e0a80c57 100644 --- a/docs/Guide-Installation-Setup-template.md +++ b/docs/Guide-Installation-Setup-template.md @@ -2,6 +2,10 @@ - [Changelog](http://www.hockeyapp.net/help/sdk/ios/4.1.0-beta.1/docs/docs/Changelog.html) +**NOTE:** With the release of HockeySDK 4.0.0-alpha.1 a bug was introduced which lead to the exclusion of the Application Support folder from iCloud and iTunes backups. + +If you have been using one of the affected versions (4.0.0-alpha.2, Version 4.0.0-beta.1, 4.0.0, 4.1.0-alpha.1, 4.1.0-alpha.2, or Version 4.1.0-beta.1), please make sure to update to at least version 4.0.1 or 4.1.0-beta.2 of our SDK as soon as you can. + ## Introduction This document contains the following sections: