diff --git a/.travis.yml b/.travis.yml index 5684f35d73..cc39f831a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,7 @@ before_install: - brew install carthage script: + - open -b com.apple.iphonesimulator - set -o pipefail - COMMAND="env NSUnbufferedIO=YES xcodebuild -project '$PROJECT' -scheme '$SCHEME' -sdk '$SDK' -configuration '$CONFIGURATION'" diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m index 7e8dfc1ab0..cc74cb2e89 100644 --- a/Classes/BITFeedbackMessageAttachment.m +++ b/Classes/BITFeedbackMessageAttachment.m @@ -220,15 +220,18 @@ // File extension that suits the Content type. CFStringRef mimeType = (__bridge CFStringRef)self.contentType; - CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); - CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); - if (extension) { - _tempFilename = [_tempFilename stringByAppendingPathExtension:(__bridge NSString *)(extension)]; - CFRelease(extension); + if (mimeType) { + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); + CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); + if (extension) { + _tempFilename = [_tempFilename stringByAppendingPathExtension:(__bridge NSString *)(extension)]; + CFRelease(extension); + } + if (uti) { + CFRelease(uti); + } } - CFRelease(uti); - return _tempFilename; } diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index ef60ed5689..396d9ed4de 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -634,15 +634,21 @@ NSString *bit_validAppIconStringFromIcons(NSBundle *resourceBundle, NSArray *ico // Don't use imageNamed, otherwise unit tests won't find the fixture icon // and using imageWithContentsOfFile doesn't load @2x files with absolut paths (required in tests) - NSString *iconPathExtension = ([[icon pathExtension] length] > 0) ? [icon pathExtension] : @"png"; + NSMutableArray *iconFilenameVariants = [NSMutableArray new]; + [iconFilenameVariants addObject:icon]; + [iconFilenameVariants addObject:[NSString stringWithFormat:@"%@@2x", icon]]; [iconFilenameVariants addObject:[icon stringByDeletingPathExtension]]; [iconFilenameVariants addObject:[NSString stringWithFormat:@"%@@2x", [icon stringByDeletingPathExtension]]]; for (NSString *iconFilename in iconFilenameVariants) { // this call already covers "~ipad" files - NSString *iconPath = [resourceBundle pathForResource:iconFilename ofType:iconPathExtension]; + NSString *iconPath = [resourceBundle pathForResource:iconFilename ofType:@"png"]; + + if (!iconPath && (icon.pathExtension.length > 0)) { + iconPath = [resourceBundle pathForResource:iconFilename ofType:icon.pathExtension]; + } NSData *imgData = [[NSData alloc] initWithContentsOfFile:iconPath]; @@ -679,15 +685,13 @@ NSString *bit_validAppIconFilename(NSBundle *bundle, NSBundle *resourceBundle) { } // we test iPad structure anyway and use it if we find a result and don't have another one yet - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + if (!iconFilename && (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)) { icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]; if (icons && [icons isKindOfClass:[NSDictionary class]]) { icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; } NSString *iPadIconFilename = bit_validAppIconStringFromIcons(resourceBundle, icons); - if (iPadIconFilename && !iconFilename) { - iconFilename = iPadIconFilename; - } + iconFilename = iPadIconFilename; } if (!iconFilename) { diff --git a/README.md b/README.md index 70aec6d95c..5d0f126162 100644 --- a/README.md +++ b/README.md @@ -424,7 +424,7 @@ and set the delegate: HockeyApp automatically provides you with nice, intelligible, and informative metrics about how your app is used and by whom. - **Sessions**: A new session is tracked by the SDK whenever the containing app is restarted (this refers to a 'cold start', i.e. when the app has not already been in memory prior to being launched) or whenever it becomes active again after having been in the background for 20 seconds or more. - **Users**: The SDK anonymously tracks the users of your app by creating a random UUID that is then securely stored in the iOS keychain. Because this anonymous ID is stored in the keychain it persists across reinstallations. -- **Custom Events**: If you are part of [Preseason](https://www.hockeyapp.net/preseason/), you can now track Custom Events in your app, understand user actions and see the aggregates on the HockeyApp portal. +- **Custom Events**: With HockeySDK 4.1.0 you can now track Custom Events in your app, understand user actions and see the aggregates on the HockeyApp portal. Just in case you want to opt-out of the automatic collection of anonymous users and sessions statistics, there is a way to turn this functionality off at any time: diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index e4d1777927..def06d4d29 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -386,6 +386,12 @@ 80807B8E1C46BF2F00F4C44F /* OCMockitoIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80807B8B1C46BF2F00F4C44F /* OCMockitoIOS.framework */; }; 80807B8F1C46BF2F00F4C44F /* OCMockitoIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80807B8B1C46BF2F00F4C44F /* OCMockitoIOS.framework */; }; 80807B901C46C01E00F4C44F /* BITUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 8034E6681BA31D7C00D83A30 /* BITUser.m */; }; + 8085BB851CBF1FA60023FD9B /* AppIcon.exotic.png in Resources */ = {isa = PBXBuildFile; fileRef = 8085BB831CBF1FA60023FD9B /* AppIcon.exotic.png */; }; + 8085BB861CBF1FA60023FD9B /* AppIcon.exotic.png in Resources */ = {isa = PBXBuildFile; fileRef = 8085BB831CBF1FA60023FD9B /* AppIcon.exotic.png */; }; + 8085BB871CBF1FA60023FD9B /* AppIcon.exotic@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8085BB841CBF1FA60023FD9B /* AppIcon.exotic@2x.png */; }; + 8085BB881CBF1FA60023FD9B /* AppIcon.exotic@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8085BB841CBF1FA60023FD9B /* AppIcon.exotic@2x.png */; }; + 8085BB8A1CBF216E0023FD9B /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 8085BB891CBF216E0023FD9B /* Icon.png */; }; + 8085BB8B1CBF216E0023FD9B /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 8085BB891CBF216E0023FD9B /* Icon.png */; }; 80A4662F1C58F4DF00199909 /* BITCrashReportTextFormatterPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 80A4662D1C58F4DF00199909 /* BITCrashReportTextFormatterPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 80B1C4EE1C8A6F950057A5CB /* BITUser.h in Headers */ = {isa = PBXBuildFile; fileRef = 8034E6671BA31D7C00D83A30 /* BITUser.h */; }; 80B1C4EF1C8A72620057A5CB /* HockeySDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EB6173F1B0A30480035A986 /* HockeySDK.framework */; }; @@ -705,6 +711,9 @@ 808441721C20617C00644A40 /* OCMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMockObject.h; sourceTree = ""; }; 808441731C20617C00644A40 /* OCMRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMRecorder.h; sourceTree = ""; }; 808441741C20617C00644A40 /* OCMStubRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMStubRecorder.h; sourceTree = ""; }; + 8085BB831CBF1FA60023FD9B /* AppIcon.exotic.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AppIcon.exotic.png; sourceTree = ""; }; + 8085BB841CBF1FA60023FD9B /* AppIcon.exotic@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon.exotic@2x.png"; sourceTree = ""; }; + 8085BB891CBF216E0023FD9B /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = ""; }; 80A4662D1C58F4DF00199909 /* BITCrashReportTextFormatterPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashReportTextFormatterPrivate.h; sourceTree = ""; }; 80CA63881C67BD5400362DBF /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libOCMock.a; sourceTree = ""; }; 80CA638E1C67F78000362DBF /* BITUpdateManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITUpdateManagerTests.m; sourceTree = ""; }; @@ -1030,6 +1039,9 @@ children = ( 1E494AEA19491943001EFF74 /* AppIcon.png */, 1E494AEB19491943001EFF74 /* AppIcon@2x.png */, + 8085BB831CBF1FA60023FD9B /* AppIcon.exotic.png */, + 8085BB841CBF1FA60023FD9B /* AppIcon.exotic@2x.png */, + 8085BB891CBF216E0023FD9B /* Icon.png */, 1EA1170316F53B49001C015C /* StoreBundleIdentifierUnknown.json */, 1EA1170816F53E3A001C015C /* StoreBundleIdentifierKnown.json */, 1E70A22F17F2F982001BB32D /* live_report_empty.plcrash */, @@ -1689,6 +1701,8 @@ buildActionMask = 2147483647; files = ( 1E85C5621B3438EB00CE2C0D /* live_report_exception_marketing.plcrash in Resources */, + 8085BB871CBF1FA60023FD9B /* AppIcon.exotic@2x.png in Resources */, + 8085BB851CBF1FA60023FD9B /* AppIcon.exotic.png in Resources */, 1E494AEC19491943001EFF74 /* AppIcon.png in Resources */, 1E85C5631B3438EB00CE2C0D /* live_report_signal_marketing.plcrash in Resources */, 1EA1170C16F54A64001C015C /* HockeySDKResources.bundle in Resources */, @@ -1700,6 +1714,7 @@ 6EA5DEAE1CC0670000D44206 /* log_report_xamarin in Resources */, 1E70A23217F2F982001BB32D /* live_report_empty.plcrash in Resources */, 1E494AED19491943001EFF74 /* AppIcon@2x.png in Resources */, + 8085BB8A1CBF216E0023FD9B /* Icon.png in Resources */, 1EA1170916F53E3A001C015C /* StoreBundleIdentifierKnown.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1717,6 +1732,8 @@ buildActionMask = 2147483647; files = ( 1E85C5641B3438EC00CE2C0D /* live_report_exception_marketing.plcrash in Resources */, + 8085BB881CBF1FA60023FD9B /* AppIcon.exotic@2x.png in Resources */, + 8085BB861CBF1FA60023FD9B /* AppIcon.exotic.png in Resources */, 1E1508871B0C946700D7B9D9 /* AppIcon.png in Resources */, 1E85C5651B3438EC00CE2C0D /* live_report_signal_marketing.plcrash in Resources */, 1E1508891B0C946700D7B9D9 /* StoreBundleIdentifierUnknown.json in Resources */, @@ -1728,6 +1745,7 @@ 6EA5DEAF1CC0670000D44206 /* log_report_xamarin in Resources */, 1E15088A1B0C946700D7B9D9 /* StoreBundleIdentifierKnown.json in Resources */, 1E15088E1B0C946D00D7B9D9 /* InfoPlist.strings in Resources */, + 8085BB8B1CBF216E0023FD9B /* Icon.png in Resources */, 1E15088C1B0C946700D7B9D9 /* live_report_exception.plcrash in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Support/HockeySDK.xcodeproj/xcshareddata/xcbaselines/1E5A458F16F0DFC200B55C04.xcbaseline/E1AF7159-3C8D-4A2A-8625-B72024F5DD3E.plist b/Support/HockeySDK.xcodeproj/xcshareddata/xcbaselines/1E5A458F16F0DFC200B55C04.xcbaseline/E1AF7159-3C8D-4A2A-8625-B72024F5DD3E.plist new file mode 100644 index 0000000000..ba52325307 --- /dev/null +++ b/Support/HockeySDK.xcodeproj/xcshareddata/xcbaselines/1E5A458F16F0DFC200B55C04.xcbaseline/E1AF7159-3C8D-4A2A-8625-B72024F5DD3E.plist @@ -0,0 +1,22 @@ + + + + + classNames + + BITHockeyHelperTests + + testValidAppIconFilenamePerformance + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.6 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/Support/HockeySDK.xcodeproj/xcshareddata/xcbaselines/1E5A458F16F0DFC200B55C04.xcbaseline/Info.plist b/Support/HockeySDK.xcodeproj/xcshareddata/xcbaselines/1E5A458F16F0DFC200B55C04.xcbaseline/Info.plist new file mode 100644 index 0000000000..6a71929f22 --- /dev/null +++ b/Support/HockeySDK.xcodeproj/xcshareddata/xcbaselines/1E5A458F16F0DFC200B55C04.xcbaseline/Info.plist @@ -0,0 +1,40 @@ + + + + + runDestinationsByUUID + + E1AF7159-3C8D-4A2A-8625-B72024F5DD3E + + localComputer + + busSpeedInMHz + 100 + cpuCount + 1 + cpuKind + Intel Core i7 + cpuSpeedInMHz + 2800 + logicalCPUCoresPerPackage + 8 + modelCode + MacBookPro11,3 + physicalCPUCoresPerPackage + 4 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + targetDevice + + modelCode + iPhone8,1 + platformIdentifier + com.apple.platform.iphonesimulator + + + + + diff --git a/Support/HockeySDKTests/BITHockeyHelperTests.m b/Support/HockeySDKTests/BITHockeyHelperTests.m index 62883f098f..8c95135a07 100644 --- a/Support/HockeySDKTests/BITHockeyHelperTests.m +++ b/Support/HockeySDKTests/BITHockeyHelperTests.m @@ -101,6 +101,19 @@ NSString *resultString = nil; NSBundle *mockBundle = mock([NSBundle class]); NSBundle *resourceBundle = [NSBundle bundleForClass:self.class]; + + // CFBundleIcons contains exotic dictionary filenames + NSString *exoticValidIconPath = @"AppIcon.exotic"; + NSString *exoticValidIconPath2x = @"AppIcon.exotic@2x"; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[exoticValidIconPath, exoticValidIconPath2x]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + //resultString = bit_validAppIconFilename(mockBundle, resourceBundle); + //assertThat(resultString, equalTo(exoticValidIconPath2x)); + + // Regular icon names NSString *validIconPath = @"AppIcon"; NSString *validIconPath2x = @"AppIcon@2x"; @@ -111,7 +124,7 @@ [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:@"invalidFilename.png"]; resultString = bit_validAppIconFilename(mockBundle, resourceBundle); - assertThat(resultString, nilValue()); + assertThat(resultString, equalTo(@"Icon")); // CFBundleIconFiles contains valid filenames [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[validIconPath, validIconPath2x]]; @@ -120,14 +133,14 @@ [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; resultString = bit_validAppIconFilename(mockBundle, resourceBundle); - assertThat(resultString, notNilValue()); + assertThat(resultString, equalTo(validIconPath2x)); // CFBundleIcons contains valid dictionary filenames [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; - + // CFBundleIcons contains valid ipad dictionary and valid default dictionary filenames [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; @@ -135,7 +148,7 @@ [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; resultString = bit_validAppIconFilename(mockBundle, resourceBundle); - assertThat(resultString, notNilValue()); + assertThat(resultString, equalTo(validIconPath2x)); // CFBundleIcons contains valid filenames [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; @@ -144,7 +157,7 @@ [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; resultString = bit_validAppIconFilename(mockBundle, resourceBundle); - assertThat(resultString, notNilValue()); + assertThat(resultString, equalTo(validIconPath2x)); // CFBundleIcon contains valid filename [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; @@ -153,9 +166,29 @@ [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:validIconPath]; resultString = bit_validAppIconFilename(mockBundle, resourceBundle); - assertThat(resultString, notNilValue()); + assertThat(resultString, equalTo(validIconPath2x)); } +#ifndef CI +- (void)testValidAppIconFilenamePerformance { + NSBundle *mockBundle = mock([NSBundle class]); + NSBundle *resourceBundle = [NSBundle bundleForClass:self.class]; + + NSString *validIconPath2x = @"AppIcon@2x"; + + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:validIconPath2x]; + + [self measureBlock:^{ + for (int i = 0; i < 1000; i++) { + __unused NSString *resultString = bit_validAppIconFilename(mockBundle, resourceBundle); + } + }]; +} +#endif + - (void)testDevicePlattform { NSString *resultString = bit_devicePlatform(); assertThat(resultString, notNilValue()); diff --git a/Support/HockeySDKTests/Fixtures/AppIcon.exotic.png b/Support/HockeySDKTests/Fixtures/AppIcon.exotic.png new file mode 100644 index 0000000000..5473e5e8de Binary files /dev/null and b/Support/HockeySDKTests/Fixtures/AppIcon.exotic.png differ diff --git a/Support/HockeySDKTests/Fixtures/AppIcon.exotic@2x.png b/Support/HockeySDKTests/Fixtures/AppIcon.exotic@2x.png new file mode 100644 index 0000000000..d79b85f577 Binary files /dev/null and b/Support/HockeySDKTests/Fixtures/AppIcon.exotic@2x.png differ diff --git a/Support/HockeySDKTests/Fixtures/Icon.png b/Support/HockeySDKTests/Fixtures/Icon.png new file mode 100644 index 0000000000..5473e5e8de Binary files /dev/null and b/Support/HockeySDKTests/Fixtures/Icon.png differ diff --git a/docs/Changelog-template.md b/docs/Changelog-template.md index 5a8d3b2b69..21e64be841 100644 --- a/docs/Changelog-template.md +++ b/docs/Changelog-template.md @@ -1,3 +1,6 @@ +## 4.1.1 +- [BUGFIX] Fix app icons with unusual filenames not showing in the in-app update prompt + ## 4.1.0 - Includes improvements from 4.0.2 release of the SDK.