diff --git a/Classes/BITHockeyBaseManager.m b/Classes/BITHockeyBaseManager.m index 1f21ff6997..334daa7e77 100644 --- a/Classes/BITHockeyBaseManager.m +++ b/Classes/BITHockeyBaseManager.m @@ -99,25 +99,7 @@ } - (BOOL)isPreiOS7Environment { - static BOOL isPreiOS7Environment = YES; - static dispatch_once_t checkOS; - - dispatch_once(&checkOS, ^{ - // we only perform this runtime check if this is build against at least iOS7 base SDK -#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 - // runtime check according to - // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { - isPreiOS7Environment = YES; - } else { - isPreiOS7Environment = NO; - } -#else - isPreiOS7Environment = YES; -#endif - }); - - return isPreiOS7Environment; + return bit_isPreiOS7Environment(); } - (NSString *)getDevicePlatform { diff --git a/Classes/BITHockeyHelper.h b/Classes/BITHockeyHelper.h index a508dc6422..9a7e1ecf5f 100644 --- a/Classes/BITHockeyHelper.h +++ b/Classes/BITHockeyHelper.h @@ -46,6 +46,10 @@ NSString *bit_appName(NSString *placeHolderString); NSString *bit_UUIDPreiOS6(void); NSString *bit_UUID(void); NSString *bit_appAnonID(void); +BOOL bit_isPreiOS7Environment(void); + +NSString *bit_validAppIconStringFromIcons(NSArray *icons); +NSString *bit_validAppIconFilename(NSBundle *bundle); /* UIImage helpers */ UIImage *bit_roundedCornerImage(UIImage *inputImage, NSInteger cornerSize, NSInteger borderSize); diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 0ab661e5f0..da8c13bdc0 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -216,6 +216,114 @@ NSString *bit_appAnonID(void) { return appAnonID; } +BOOL bit_isPreiOS7Environment(void) { + static BOOL isPreiOS7Environment = YES; + static dispatch_once_t checkOS; + + dispatch_once(&checkOS, ^{ + // we only perform this runtime check if this is build against at least iOS7 base SDK +#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 + // runtime check according to + // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { + isPreiOS7Environment = YES; + } else { + isPreiOS7Environment = NO; + } +#else + isPreiOS7Environment = YES; +#endif + }); + + return isPreiOS7Environment; +} + +/** + Find a valid app icon filename that points to a proper app icon image + + @param icons NSArray with app icon filenames + + @return NSString with the valid app icon or nil if none found + */ +NSString *bit_validAppIconStringFromIcons(NSArray *icons) { + if (!icons) return nil; + if (![icons isKindOfClass:[NSArray class]]) return nil; + + BOOL useHighResIcon = NO; + BOOL useiPadIcon = NO; + if ([UIScreen mainScreen].scale == 2.0f) useHighResIcon = YES; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) useiPadIcon = YES; + + NSString *currentBestMatch = nil; + float currentBestMatchHeight = 0; + float bestMatchHeight = 0; + + if (bit_isPreiOS7Environment()) { + bestMatchHeight = useiPadIcon ? (useHighResIcon ? 144 : 72) : (useHighResIcon ? 114 : 57); + } else { + bestMatchHeight = useiPadIcon ? (useHighResIcon ? 152 : 76) : 120; + } + + for(NSString *icon in icons) { + // 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) + NSData *imgData = [[NSData alloc] initWithContentsOfFile:icon]; + UIImage *iconImage = [[UIImage alloc] initWithData:imgData]; + + if (iconImage) { + if (iconImage.size.height == bestMatchHeight) { + return icon; + } else if (iconImage.size.height < bestMatchHeight && + iconImage.size.height > currentBestMatchHeight) { + currentBestMatchHeight = iconImage.size.height; + currentBestMatch = icon; + } + } + } + + return currentBestMatch; +} + +NSString *bit_validAppIconFilename(NSBundle *bundle) { + NSString *iconFilename = nil; + NSArray *icons = nil; + + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]; + iconFilename = bit_validAppIconStringFromIcons(icons); + + if (!iconFilename) { + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons"]; + if (icons && [icons isKindOfClass:[NSDictionary class]]) { + icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + } + iconFilename = bit_validAppIconStringFromIcons(icons); + } + + // 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) { + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]; + if (icons && [icons isKindOfClass:[NSDictionary class]]) { + icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + } + NSString *iPadIconFilename = bit_validAppIconStringFromIcons(icons); + if (iPadIconFilename && !iconFilename) { + iconFilename = iPadIconFilename; + } + } + + if (!iconFilename) { + NSString *tempFilename = [bundle objectForInfoDictionaryKey:@"CFBundleIconFile"]; + if (tempFilename) { + iconFilename = bit_validAppIconStringFromIcons(@[tempFilename]); + } + } + + if (!iconFilename) { + iconFilename = bit_validAppIconStringFromIcons(@[@"Icon.png"]); + } + + return iconFilename; +} #pragma mark UIImage private helpers diff --git a/Classes/BITUpdateViewController.m b/Classes/BITUpdateViewController.m index 28c99f6450..e75e22b19c 100644 --- a/Classes/BITUpdateViewController.m +++ b/Classes/BITUpdateViewController.m @@ -294,57 +294,19 @@ } [self updateAppStoreHeader]; - NSString *iconString = nil; - NSArray *icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFiles"]; - if (!icons) { - icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIcons"]; - if ((icons) && ([icons isKindOfClass:[NSDictionary class]])) { - icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + NSString *iconFilename = bit_validAppIconFilename([NSBundle mainBundle]); + if (iconFilename) { + BOOL addGloss = YES; + NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; + if (prerendered) { + addGloss = ![prerendered boolValue]; } - if (!icons) { - iconString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFile"]; - if (!iconString) { - iconString = @"Icon.png"; - } + if (addGloss && [self.updateManager isPreiOS7Environment]) { + _appStoreHeader.iconImage = [self addGlossToImage:[UIImage imageNamed:iconFilename]]; + } else { + _appStoreHeader.iconImage = [UIImage imageNamed:iconFilename]; } - } - - if (icons) { - BOOL useHighResIcon = NO; - BOOL useiPadIcon = NO; - if ([UIScreen mainScreen].scale == 2.0f) useHighResIcon = YES; - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) useiPadIcon = YES; - - for(NSString *icon in icons) { - iconString = icon; - UIImage *iconImage = [UIImage imageNamed:icon]; - - if ( - (iconImage.size.height == 57 && !useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 114 && useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 120 && useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 72 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 76 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 144 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 152 && useHighResIcon && useiPadIcon) - ) { - // found! - break; - } - } - } - - BOOL addGloss = YES; - NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; - if (prerendered) { - addGloss = ![prerendered boolValue]; - } - - if (addGloss && [self.updateManager isPreiOS7Environment]) { - _appStoreHeader.iconImage = [self addGlossToImage:[UIImage imageNamed:iconString]]; - } else { - _appStoreHeader.iconImage = [UIImage imageNamed:iconString]; } self.tableView.tableHeaderView = _appStoreHeader; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 1115044ead..7e4c4843aa 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ 1E1127C916580C87007067A2 /* buttonRoundedRegular@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C116580C87007067A2 /* buttonRoundedRegular@2x.png */; }; 1E1127CA16580C87007067A2 /* buttonRoundedRegularHighlighted.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C216580C87007067A2 /* buttonRoundedRegularHighlighted.png */; }; 1E1127CB16580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C316580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png */; }; + 1E494AEC19491943001EFF74 /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E494AEA19491943001EFF74 /* AppIcon.png */; }; + 1E494AED19491943001EFF74 /* AppIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E494AEB19491943001EFF74 /* AppIcon@2x.png */; }; 1E49A43C1612223B00463151 /* BITFeedbackComposeViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1E49A43F1612223B00463151 /* BITFeedbackComposeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */; }; 1E49A4421612223B00463151 /* BITFeedbackListViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E49A42F1612223B00463151 /* BITFeedbackListViewCell.h */; }; @@ -226,6 +228,8 @@ 1E1127C316580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "buttonRoundedRegularHighlighted@2x.png"; sourceTree = ""; }; 1E20A57F181E9D4600D5B770 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/HockeySDK.strings; sourceTree = ""; }; 1E36D8B816667611000B134C /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/HockeySDK.strings; sourceTree = ""; }; + 1E494AEA19491943001EFF74 /* AppIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AppIcon.png; sourceTree = ""; }; + 1E494AEB19491943001EFF74 /* AppIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon@2x.png"; sourceTree = ""; }; 1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackComposeViewController.h; sourceTree = ""; }; 1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITFeedbackComposeViewController.m; sourceTree = ""; }; 1E49A42F1612223B00463151 /* BITFeedbackListViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackListViewCell.h; sourceTree = ""; }; @@ -601,6 +605,8 @@ 1EA1170216F53B49001C015C /* Fixtures */ = { isa = PBXGroup; children = ( + 1E494AEA19491943001EFF74 /* AppIcon.png */, + 1E494AEB19491943001EFF74 /* AppIcon@2x.png */, 1EA1170316F53B49001C015C /* StoreBundleIdentifierUnknown.json */, 1EA1170816F53E3A001C015C /* StoreBundleIdentifierKnown.json */, 1E70A22F17F2F982001BB32D /* live_report_empty.plcrash */, @@ -949,12 +955,14 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E494AEC19491943001EFF74 /* AppIcon.png in Resources */, 1EA1170C16F54A64001C015C /* HockeySDKResources.bundle in Resources */, 1E5A459B16F0DFC200B55C04 /* InfoPlist.strings in Resources */, 1E70A23417F2F982001BB32D /* live_report_signal.plcrash in Resources */, 1EA1170416F53B49001C015C /* StoreBundleIdentifierUnknown.json in Resources */, 1E70A23317F2F982001BB32D /* live_report_exception.plcrash in Resources */, 1E70A23217F2F982001BB32D /* live_report_empty.plcrash in Resources */, + 1E494AED19491943001EFF74 /* AppIcon@2x.png in Resources */, 1EA1170916F53E3A001C015C /* StoreBundleIdentifierKnown.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Support/HockeySDKTests/BITHockeyHelperTests.m b/Support/HockeySDKTests/BITHockeyHelperTests.m index 315a7c4238..3cd800ade3 100644 --- a/Support/HockeySDKTests/BITHockeyHelperTests.m +++ b/Support/HockeySDKTests/BITHockeyHelperTests.m @@ -90,5 +90,63 @@ assertThatInteger([resultString length], equalToInteger(36)); } +- (void)testValidAppIconFilename { + NSString *resultString = nil; + NSBundle *mockBundle = mock([NSBundle class]); + NSString *validIconPath = [[NSBundle bundleForClass:self.class] pathForResource:@"AppIcon" ofType:@"png"]; + NSString *validIconPath2x = [[NSBundle bundleForClass:self.class] pathForResource:@"AppIcon@2x" ofType:@"png"]; + + // No valid icons defined at all + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:@"invalidFilename.png"]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, nilValue()); + + // CFBundleIconFiles contains valid filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[validIconPath, validIconPath2x]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // 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]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // CFBundleIcons contains valid filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@[validIconPath, validIconPath2x]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // CFBundleIcon contains valid filename + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:validIconPath]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); +} + @end diff --git a/Support/HockeySDKTests/Fixtures/AppIcon.png b/Support/HockeySDKTests/Fixtures/AppIcon.png new file mode 100644 index 0000000000..5473e5e8de Binary files /dev/null and b/Support/HockeySDKTests/Fixtures/AppIcon.png differ diff --git a/Support/HockeySDKTests/Fixtures/AppIcon@2x.png b/Support/HockeySDKTests/Fixtures/AppIcon@2x.png new file mode 100644 index 0000000000..d79b85f577 Binary files /dev/null and b/Support/HockeySDKTests/Fixtures/AppIcon@2x.png differ