/* * Author: Andreas Linde * * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. * All rights reserved. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #import "BITHockeyHelper+Application.h" #import "BITKeychainUtils.h" #import "HockeySDK.h" #import "HockeySDKPrivate.h" #if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnly) && !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) #import #endif #import #import static NSString *const kBITUtcDateFormatter = @"utcDateFormatter"; NSString *const kBITExcludeApplicationSupportFromBackup = @"kBITExcludeApplicationSupportFromBackup"; @implementation BITHockeyHelper /** * @discussion * Workaround for exporting symbols from category object files. * See article https://medium.com/ios-os-x-development/categories-in-static-libraries-78e41f8ddb96#.aedfl1kl0 */ __attribute__((used)) static void importCategories() { [NSString stringWithFormat:@"%@", BITHockeyHelperApplicationCategory]; } + (BOOL)isURLSessionSupported { id nsurlsessionClass = NSClassFromString(@"NSURLSessionUploadTask"); BOOL isUrlSessionSupported = (nsurlsessionClass && !bit_isRunningInAppExtension()); return isUrlSessionSupported; } + (BOOL)isPhotoAccessPossible { if(bit_isPreiOS10Environment()) { return YES; } else { NSString *privacyDescription = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSPhotoLibraryUsageDescription"]; BOOL privacyStringSet = (privacyDescription != nil) && (privacyDescription.length > 0); return privacyStringSet; } } @end typedef struct { uint8_t info_version; const char bit_version[16]; const char bit_build[16]; } bit_info_t; static bit_info_t hockeyapp_library_info __attribute__((section("__TEXT,__bit_ios,regular,no_dead_strip"))) = { .info_version = 1, .bit_version = BITHOCKEY_C_VERSION, .bit_build = BITHOCKEY_C_BUILD }; #pragma mark - Helpers NSString *bit_settingsDir(void) { static NSString *settingsDir = nil; static dispatch_once_t predSettingsDir; dispatch_once(&predSettingsDir, ^{ NSFileManager *fileManager = [[NSFileManager alloc] init]; // temporary directory for crashes grabbed from PLCrashReporter NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); settingsDir = [[paths objectAtIndex:0] stringByAppendingPathComponent:BITHOCKEY_IDENTIFIER]; if (![fileManager fileExistsAtPath:settingsDir]) { NSDictionary *attributes = [NSDictionary dictionaryWithObject: [NSNumber numberWithUnsignedLong: 0755] forKey: NSFilePosixPermissions]; NSError *theError = NULL; [fileManager createDirectoryAtPath:settingsDir withIntermediateDirectories: YES attributes: attributes error: &theError]; } }); return settingsDir; } BOOL bit_validateEmail(NSString *email) { NSString *emailRegex = @"(?:[a-z0-9!#$%\\&'*+/=?\\^_`{|}~-]+(?:\\.[a-z0-9!#$%\\&'*+/=?\\^_`{|}" @"~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\" @"x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-" @"z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5" @"]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-" @"9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21" @"-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"; NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES[c] %@", emailRegex]; return [emailTest evaluateWithObject:email]; } NSString *bit_keychainHockeySDKServiceName(void) { static NSString *serviceName = nil; static dispatch_once_t predServiceName; dispatch_once(&predServiceName, ^{ serviceName = [NSString stringWithFormat:@"%@.HockeySDK", bit_mainBundleIdentifier()]; }); return serviceName; } NSComparisonResult bit_versionCompare(NSString *stringA, NSString *stringB) { // Extract plain version number from self NSString *plainSelf = stringA; NSRange letterRange = [plainSelf rangeOfCharacterFromSet: [NSCharacterSet letterCharacterSet]]; if (letterRange.length) plainSelf = [plainSelf substringToIndex: letterRange.location]; // Extract plain version number from other NSString *plainOther = stringB; letterRange = [plainOther rangeOfCharacterFromSet: [NSCharacterSet letterCharacterSet]]; if (letterRange.length) plainOther = [plainOther substringToIndex: letterRange.location]; // Compare plain versions NSComparisonResult result = [plainSelf compare:plainOther options:NSNumericSearch]; // If plain versions are equal, compare full versions if (result == NSOrderedSame) result = [stringA compare:stringB options:NSNumericSearch]; // Done return result; } #pragma mark Exclude from backup fix void bit_fixBackupAttributeForURL(NSURL *directoryURL) { BOOL shouldExcludeAppSupportDirFromBackup = [[NSUserDefaults standardUserDefaults] boolForKey:kBITExcludeApplicationSupportFromBackup]; if (shouldExcludeAppSupportDirFromBackup) { return; } if (directoryURL) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *getResourceError = nil; NSNumber *appSupportDirExcludedValue; if ([directoryURL getResourceValue:&appSupportDirExcludedValue forKey:NSURLIsExcludedFromBackupKey error:&getResourceError] && appSupportDirExcludedValue) { NSError *setResourceError = nil; if(![directoryURL setResourceValue:@NO forKey:NSURLIsExcludedFromBackupKey error:&setResourceError]) { BITHockeyLogError(@"ERROR: Error while setting resource value: %@", setResourceError.localizedDescription); } } else { BITHockeyLogError(@"ERROR: Error while retrieving resource value: %@", getResourceError.localizedDescription); } }); } } #pragma mark Identifiers NSString *bit_mainBundleIdentifier(void) { return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; } NSString *bit_encodeAppIdentifier(NSString *inputString) { return (inputString ? bit_URLEncodedString(inputString) : bit_URLEncodedString(bit_mainBundleIdentifier())); } NSString *bit_appIdentifierToGuid(NSString *appIdentifier) { NSMutableString *guid; NSString *cleanAppId = [appIdentifier stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if(cleanAppId && cleanAppId.length == 32) { // Insert dashes so that DC will accept th appidentifier (as a replacement for iKey) guid = [NSMutableString stringWithString:cleanAppId]; [guid insertString:@"-" atIndex:20]; [guid insertString:@"-" atIndex:16]; [guid insertString:@"-" atIndex:12]; [guid insertString:@"-" atIndex:8]; } return [guid copy]; } NSString *bit_appName(NSString *placeHolderString) { NSString *appName = [[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleDisplayName"]; if (!appName) appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; if (!appName) appName = [[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleName"]; if (!appName) appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"] ?: placeHolderString; return appName; } NSString *bit_UUID(void) { return [[NSUUID UUID] UUIDString]; } NSString *bit_appAnonID(BOOL forceNewAnonID) { static NSString *appAnonID = nil; static dispatch_once_t predAppAnonID; __block NSError *error = nil; NSString *appAnonIDKey = @"appAnonID"; if (forceNewAnonID) { appAnonID = bit_UUID(); // store this UUID in the keychain (on this device only) so we can be sure to always have the same ID upon app startups if (appAnonID) { // add to keychain in a background thread, since we got reports that storing to the keychain may take several seconds sometimes and cause the app to be killed // and we don't care about the result anyway dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ [BITKeychainUtils storeUsername:appAnonIDKey andPassword:appAnonID forServiceName:bit_keychainHockeySDKServiceName() updateExisting:YES accessibility:kSecAttrAccessibleAlwaysThisDeviceOnly error:&error]; }); } } else { dispatch_once(&predAppAnonID, ^{ // first check if we already have an install string in the keychain appAnonID = [BITKeychainUtils getPasswordForUsername:appAnonIDKey andServiceName:bit_keychainHockeySDKServiceName() error:&error]; if (!appAnonID) { appAnonID = bit_UUID(); // store this UUID in the keychain (on this device only) so we can be sure to always have the same ID upon app startups if (appAnonID) { // add to keychain in a background thread, since we got reports that storing to the keychain may take several seconds sometimes and cause the app to be killed // and we don't care about the result anyway dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ [BITKeychainUtils storeUsername:appAnonIDKey andPassword:appAnonID forServiceName:bit_keychainHockeySDKServiceName() updateExisting:YES accessibility:kSecAttrAccessibleAlwaysThisDeviceOnly error:&error]; }); } } }); } return appAnonID; } #pragma mark Environment detection BOOL bit_isPreiOS10Environment(void) { static BOOL isPreOS10Environment = YES; static dispatch_once_t checkOS10; dispatch_once(&checkOS10, ^{ // NSFoundationVersionNumber_iOS_9_MAX = 1299 // We hardcode this, so compiling with iOS 7 is possible while still being able to detect the correct environment // runtime check according to // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html if (floor(NSFoundationVersionNumber) <= 1299.00) { isPreOS10Environment = YES; } else { isPreOS10Environment = NO; } }); return isPreOS10Environment; } BOOL bit_isAppStoreReceiptSandbox(void) { #if TARGET_OS_SIMULATOR return NO; #else if (![NSBundle.mainBundle respondsToSelector:@selector(appStoreReceiptURL)]) { return NO; } NSURL *appStoreReceiptURL = NSBundle.mainBundle.appStoreReceiptURL; NSString *appStoreReceiptLastComponent = appStoreReceiptURL.lastPathComponent; BOOL isSandboxReceipt = [appStoreReceiptLastComponent isEqualToString:@"sandboxReceipt"]; return isSandboxReceipt; #endif } BOOL bit_hasEmbeddedMobileProvision(void) { BOOL hasEmbeddedMobileProvision = !![[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"]; return hasEmbeddedMobileProvision; } BITEnvironment bit_currentAppEnvironment(void) { #if TARGET_OS_SIMULATOR return BITEnvironmentOther; #else // MobilePovision profiles are a clear indicator for Ad-Hoc distribution if (bit_hasEmbeddedMobileProvision()) { return BITEnvironmentOther; } if (bit_isAppStoreReceiptSandbox()) { return BITEnvironmentTestFlight; } return BITEnvironmentAppStore; #endif } BOOL bit_isRunningInAppExtension(void) { static BOOL isRunningInAppExtension = NO; static dispatch_once_t checkAppExtension; dispatch_once(&checkAppExtension, ^{ isRunningInAppExtension = ([[[NSBundle mainBundle] executablePath] rangeOfString:@".appex/"].location != NSNotFound); }); return isRunningInAppExtension; } BOOL bit_isDebuggerAttached(void) { static BOOL debuggerIsAttached = NO; static dispatch_once_t debuggerPredicate; dispatch_once(&debuggerPredicate, ^{ struct kinfo_proc info; size_t info_size = sizeof(info); int name[4]; name[0] = CTL_KERN; name[1] = KERN_PROC; name[2] = KERN_PROC_PID; name[3] = getpid(); if (sysctl(name, 4, &info, &info_size, NULL, 0) == -1) { BITHockeyLogError(@"[HockeySDK] ERROR: Checking for a running debugger via sysctl() failed."); debuggerIsAttached = false; } if (!debuggerIsAttached && (info.kp_proc.p_flag & P_TRACED) != 0) debuggerIsAttached = true; }); return debuggerIsAttached; } #pragma mark NSString helpers NSString *bit_URLEncodedString(NSString *inputString) { return [inputString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@"!*'();:@&=+$,/?%#[] {}"].invertedSet]; } #pragma mark Context helpers // Return ISO 8601 string representation of the date NSString *bit_utcDateString(NSDate *date){ static NSDateFormatter *dateFormatter; static dispatch_once_t dateFormatterToken; dispatch_once(&dateFormatterToken, ^{ NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter = [NSDateFormatter new]; dateFormatter.locale = enUSPOSIXLocale; dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; }); NSString *dateString = [dateFormatter stringFromDate:date]; return dateString; } NSString *bit_devicePlatform(void) { size_t size; sysctlbyname("hw.machine", NULL, &size, NULL, 0); char *answer = (char*)malloc(size); if (answer == NULL) return @""; sysctlbyname("hw.machine", answer, &size, NULL, 0); NSString *platform = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding]; free(answer); return platform; } NSString *bit_deviceType(void){ UIUserInterfaceIdiom idiom = [UIDevice currentDevice].userInterfaceIdiom; switch (idiom) { case UIUserInterfaceIdiomPad: return @"Tablet"; case UIUserInterfaceIdiomPhone: return @"Phone"; default: return @"Unknown"; } } NSString *bit_osVersionBuild(void) { void *result = NULL; size_t result_len = 0; int ret; /* If our buffer is too small after allocation, loop until it succeeds -- the requested destination size * may change after each iteration. */ do { /* Fetch the expected length */ if ((ret = sysctlbyname("kern.osversion", NULL, &result_len, NULL, 0)) == -1) { break; } /* Allocate the destination buffer */ if (result != NULL) { free(result); } result = malloc(result_len); /* Fetch the value */ ret = sysctlbyname("kern.osversion", result, &result_len, NULL, 0); } while (ret == -1 && errno == ENOMEM); /* Handle failure */ if (ret == -1) { int saved_errno = errno; if (result != NULL) { free(result); } errno = saved_errno; return NULL; } NSString *osBuild = [NSString stringWithCString:result encoding:NSUTF8StringEncoding]; free(result); NSString *osVersion = [[UIDevice currentDevice] systemVersion]; return [NSString stringWithFormat:@"%@ (%@)", osVersion, osBuild]; } NSString *bit_osName(void){ return [[UIDevice currentDevice] systemName]; } NSString *bit_deviceLocale(void) { NSLocale *locale = [NSLocale currentLocale]; return [locale objectForKey:NSLocaleIdentifier]; } NSString *bit_deviceLanguage(void) { return [[NSBundle mainBundle] preferredLocalizations][0]; } NSString *bit_screenSize(void){ CGFloat scale = [UIScreen mainScreen].scale; CGSize screenSize = [UIScreen mainScreen].bounds.size; return [NSString stringWithFormat:@"%dx%d",(int)(screenSize.height * scale), (int)(screenSize.width * scale)]; } NSString *bit_sdkVersion(void){ return [NSString stringWithFormat:@"ios:%@", [NSString stringWithUTF8String:hockeyapp_library_info.bit_version]]; } NSString *bit_appVersion(void){ NSString *build = [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"]; NSString *version = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"]; if(version){ return [NSString stringWithFormat:@"%@ (%@)", version, build]; }else{ return build; } } #if !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnly) && !defined (HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions) #pragma mark AppIcon helpers /** 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(NSBundle *resourceBundle, NSArray *icons) { if (!icons) return nil; if (![icons isKindOfClass:[NSArray class]]) return nil; BOOL useHighResIcon = NO; BOOL useiPadIcon = NO; if ([UIScreen mainScreen].scale >= (CGFloat) 2.0) useHighResIcon = YES; if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) useiPadIcon = YES; NSString *currentBestMatch = nil; CGFloat currentBestMatchHeight = 0; CGFloat bestMatchHeight = 0; 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) 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:@"png"]; if (!iconPath && (icon.pathExtension.length > 0)) { iconPath = [resourceBundle pathForResource:iconFilename ofType:icon.pathExtension]; } // We still haven't managed to get a path to the app icon, just using a placeholder now. if(!iconPath) { iconPath = [resourceBundle pathForResource:@"AppIconPlaceHolder" ofType:@"png"]; } NSData *imgData = [[NSData alloc] initWithContentsOfFile:iconPath]; UIImage *iconImage = [[UIImage alloc] initWithData:imgData]; if (iconImage) { if (iconImage.size.height == bestMatchHeight) { return iconFilename; } else if (iconImage.size.height < bestMatchHeight && iconImage.size.height > currentBestMatchHeight) { currentBestMatchHeight = iconImage.size.height; currentBestMatch = iconFilename; } } } } return currentBestMatch; } NSString *bit_validAppIconFilename(NSBundle *bundle, NSBundle *resourceBundle) { NSString *iconFilename = nil; NSArray *icons = nil; icons = [bundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]; iconFilename = bit_validAppIconStringFromIcons(resourceBundle, icons); if (!iconFilename) { icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons"]; if (icons && [icons isKindOfClass:[NSDictionary class]]) { icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; } iconFilename = bit_validAppIconStringFromIcons(resourceBundle, icons); } // we test iPad structure anyway and use it if we find a result and don't have another one yet 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); iconFilename = iPadIconFilename; } if (!iconFilename) { NSString *tempFilename = [bundle objectForInfoDictionaryKey:@"CFBundleIconFile"]; if (tempFilename) { iconFilename = bit_validAppIconStringFromIcons(resourceBundle, @[tempFilename]); } } if (!iconFilename) { iconFilename = bit_validAppIconStringFromIcons(resourceBundle, @[@"Icon.png"]); } return iconFilename; } #pragma mark UIImage private helpers static void bit_addRoundedRectToPath(CGRect rect, CGContextRef context, CGFloat ovalWidth, CGFloat ovalHeight); static CGContextRef bit_MyOpenBitmapContext(int pixelsWide, int pixelsHigh); static CGImageRef bit_CreateGradientImage(int pixelsWide, int pixelsHigh, CGFloat fromAlpha, CGFloat toAlpha); static BOOL bit_hasAlpha(UIImage *inputImage); UIImage *bit_imageWithAlpha(UIImage *inputImage); UIImage *bit_addGlossToImage(UIImage *inputImage); // Adds a rectangular path to the given context and rounds its corners by the given extents // Original author: Björn Sållarp. Used with permission. See: http://blog.sallarp.com/iphone-uiimage-round-corners/ void bit_addRoundedRectToPath(CGRect rect, CGContextRef context, CGFloat ovalWidth, CGFloat ovalHeight) { if (ovalWidth == 0 || ovalHeight == 0) { CGContextAddRect(context, rect); return; } CGContextSaveGState(context); CGContextTranslateCTM(context, CGRectGetMinX(rect), CGRectGetMinY(rect)); CGContextScaleCTM(context, ovalWidth, ovalHeight); CGFloat fw = CGRectGetWidth(rect) / ovalWidth; CGFloat fh = CGRectGetHeight(rect) / ovalHeight; CGContextMoveToPoint(context, fw, fh/2); CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1); CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1); CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1); CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1); CGContextClosePath(context); CGContextRestoreGState(context); } CGImageRef bit_CreateGradientImage(int pixelsWide, int pixelsHigh, CGFloat fromAlpha, CGFloat toAlpha) { CGImageRef theCGImage = NULL; // gradient is always black-white and the mask must be in the gray colorspace CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); // create the bitmap context CGContextRef gradientBitmapContext = CGBitmapContextCreate(NULL, pixelsWide, pixelsHigh, 8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaNone); // define the start and end grayscale values (with the alpha, even though // our bitmap context doesn't support alpha the gradient requires it) CGFloat colors[] = {toAlpha, 1.0, fromAlpha, 1.0}; // create the CGGradient and then release the gray color space CGGradientRef grayScaleGradient = CGGradientCreateWithColorComponents(colorSpace, colors, NULL, 2); CGColorSpaceRelease(colorSpace); // create the start and end points for the gradient vector (straight down) CGPoint gradientEndPoint = CGPointZero; CGPoint gradientStartPoint = CGPointMake(0, pixelsHigh); // draw the gradient into the gray bitmap context CGContextDrawLinearGradient(gradientBitmapContext, grayScaleGradient, gradientStartPoint, gradientEndPoint, kCGGradientDrawsAfterEndLocation); CGGradientRelease(grayScaleGradient); // convert the context into a CGImageRef and release the context theCGImage = CGBitmapContextCreateImage(gradientBitmapContext); CGContextRelease(gradientBitmapContext); // return the imageref containing the gradient return theCGImage; } CGContextRef bit_MyOpenBitmapContext(int pixelsWide, int pixelsHigh) { CGSize size = CGSizeMake(pixelsWide, pixelsHigh); UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); return UIGraphicsGetCurrentContext(); } // Returns true if the image has an alpha layer BOOL bit_hasAlpha(UIImage *inputImage) { CGImageAlphaInfo alpha = CGImageGetAlphaInfo(inputImage.CGImage); return (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); } // Returns a copy of the given image, adding an alpha channel if it doesn't already have one UIImage *bit_imageWithAlpha(UIImage *inputImage) { if (bit_hasAlpha(inputImage)) { return inputImage; } CGImageRef imageRef = inputImage.CGImage; size_t width = (size_t)(CGImageGetWidth(imageRef) * inputImage.scale); size_t height = (size_t)(CGImageGetHeight(imageRef) * inputImage.scale); // The bitsPerComponent and bitmapInfo values are hard-coded to prevent an "unsupported parameter combination" error CGContextRef offscreenContext = CGBitmapContextCreate(NULL, width, height, 8, 0, CGImageGetColorSpace(imageRef), kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); // Draw the image into the context and retrieve the new image, which will now have an alpha layer CGContextDrawImage(offscreenContext, CGRectMake(0, 0, width, height), imageRef); CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage(offscreenContext); UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha]; // Clean up CGContextRelease(offscreenContext); CGImageRelease(imageRefWithAlpha); return imageWithAlpha; } UIImage *bit_addGlossToImage(UIImage *inputImage) { UIGraphicsBeginImageContextWithOptions(inputImage.size, NO, 0.0); [inputImage drawAtPoint:CGPointZero]; UIImage *iconGradient = bit_imageNamed(@"IconGradient.png", BITHOCKEYSDK_BUNDLE); [iconGradient drawInRect:CGRectMake(0, 0, inputImage.size.width, inputImage.size.height) blendMode:kCGBlendModeNormal alpha:0.5]; UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return result; } #pragma mark UIImage helpers UIImage *bit_imageToFitSize(UIImage *inputImage, CGSize fitSize, BOOL honorScaleFactor) { if (!inputImage){ return nil; } CGFloat imageScaleFactor = 1.0; if (honorScaleFactor) { if ([inputImage respondsToSelector:@selector(scale)]) { imageScaleFactor = [inputImage scale]; } } CGFloat sourceWidth = [inputImage size].width * imageScaleFactor; CGFloat sourceHeight = [inputImage size].height * imageScaleFactor; CGFloat targetWidth = fitSize.width; CGFloat targetHeight = fitSize.height; // Calculate aspect ratios CGFloat sourceRatio = sourceWidth / sourceHeight; CGFloat targetRatio = targetWidth / targetHeight; // Determine what side of the source image to use for proportional scaling BOOL scaleWidth = (sourceRatio <= targetRatio); // Deal with the case of just scaling proportionally to fit, without cropping scaleWidth = !scaleWidth; // Proportionally scale source image CGFloat scalingFactor, scaledWidth, scaledHeight; if (scaleWidth) { scalingFactor = ((CGFloat)1.0) / sourceRatio; scaledWidth = targetWidth; scaledHeight = round(targetWidth * scalingFactor); } else { scalingFactor = sourceRatio; scaledWidth = round(targetHeight * scalingFactor); scaledHeight = targetHeight; } // Calculate compositing rectangles CGRect sourceRect, destRect; sourceRect = CGRectMake(0, 0, sourceWidth, sourceHeight); destRect = CGRectMake(0, 0, scaledWidth, scaledHeight); // Create appropriately modified image. UIImage *image = nil; UIGraphicsBeginImageContextWithOptions(destRect.size, NO, honorScaleFactor ? 0.0 : 1.0); // 0.0 for scale means "correct scale for device's main screen". CGImageRef sourceImg = CGImageCreateWithImageInRect([inputImage CGImage], sourceRect); // cropping happens here. image = [UIImage imageWithCGImage:sourceImg scale:0.0 orientation:inputImage.imageOrientation]; // create cropped UIImage. [image drawInRect:destRect]; // the actual scaling happens here, and orientation is taken care of automatically. CGImageRelease(sourceImg); image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); if (!image) { // Try older method. CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(NULL, (size_t)scaledWidth, (size_t)scaledHeight, 8, (size_t)(fitSize.width * 4), colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); sourceImg = CGImageCreateWithImageInRect([inputImage CGImage], sourceRect); CGContextDrawImage(context, destRect, sourceImg); CGImageRelease(sourceImg); CGImageRef finalImage = CGBitmapContextCreateImage(context); CGContextRelease(context); CGColorSpaceRelease(colorSpace); image = [UIImage imageWithCGImage:finalImage]; CGImageRelease(finalImage); } return image; } UIImage *bit_reflectedImageWithHeight(UIImage *inputImage, NSUInteger height, CGFloat fromAlpha, CGFloat toAlpha) { if(height == 0) return nil; // create a bitmap graphics context the size of the image CGContextRef mainViewContentContext = bit_MyOpenBitmapContext((int)inputImage.size.width, (int)height); // create a 2 bit CGImage containing a gradient that will be used for masking the // main view content to create the 'fade' of the reflection. The CGImageCreateWithMask // function will stretch the bitmap image as required, so we can create a 1 pixel wide gradient CGImageRef gradientMaskImage = bit_CreateGradientImage(1, (int)height, fromAlpha, toAlpha); // create an image by masking the bitmap of the mainView content with the gradient view // then release the pre-masked content bitmap and the gradient bitmap CGContextClipToMask(mainViewContentContext, CGRectMake(0.0, 0.0, inputImage.size.width, height), gradientMaskImage); CGImageRelease(gradientMaskImage); // draw the image into the bitmap context CGContextDrawImage(mainViewContentContext, CGRectMake(0, 0, inputImage.size.width, inputImage.size.height), inputImage.CGImage); // convert the finished reflection image to a UIImage UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); // returns autoreleased UIGraphicsEndImageContext(); return theImage; } UIImage *bit_newWithContentsOfResolutionIndependentFile(NSString * path) { if ([UIScreen instancesRespondToSelector:@selector(scale)] && (int)[[UIScreen mainScreen] scale] == 2.0) { NSString *path2x = [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@@2x.%@", [[path lastPathComponent] stringByDeletingPathExtension], [path pathExtension]]]; if ([[NSFileManager defaultManager] fileExistsAtPath:path2x]) { return [[UIImage alloc] initWithContentsOfFile:path2x]; } } return [[UIImage alloc] initWithContentsOfFile:path]; } UIImage *bit_imageWithContentsOfResolutionIndependentFile(NSString *path) { return bit_newWithContentsOfResolutionIndependentFile(path); } UIImage *bit_imageNamed(NSString *imageName, NSString *bundleName) { NSString *resourcePath = [[NSBundle bundleForClass:[BITHockeyManager class]] resourcePath]; NSString *bundlePath = [resourcePath stringByAppendingPathComponent:bundleName]; NSString *imagePath = [bundlePath stringByAppendingPathComponent:imageName]; return bit_imageWithContentsOfResolutionIndependentFile(imagePath); } // Creates a copy of this image with rounded corners // If borderSize is non-zero, a transparent border of the given size will also be added // Original author: Björn Sållarp. Used with permission. See: http://blog.sallarp.com/iphone-uiimage-round-corners/ UIImage *bit_roundedCornerImage(UIImage *inputImage, CGFloat cornerSize, NSInteger borderSize) { // If the image does not have an alpha layer, add one UIImage *roundedImage = nil; UIGraphicsBeginImageContextWithOptions(inputImage.size, NO, 0.0); // 0.0 for scale means "correct scale for device's main screen". CGImageRef sourceImg = CGImageCreateWithImageInRect([inputImage CGImage], CGRectMake(0, 0, inputImage.size.width * inputImage.scale, inputImage.size.height * inputImage.scale)); // cropping happens here. // Create a clipping path with rounded corners CGContextRef context = UIGraphicsGetCurrentContext(); CGContextBeginPath(context); bit_addRoundedRectToPath(CGRectMake(borderSize, borderSize, inputImage.size.width - borderSize * 2, inputImage.size.height - borderSize * 2), context, cornerSize, cornerSize); CGContextClosePath(context); CGContextClip(context); roundedImage = [UIImage imageWithCGImage:sourceImg scale:0.0 orientation:inputImage.imageOrientation]; // create cropped UIImage. [roundedImage drawInRect:CGRectMake(0, 0, inputImage.size.width, inputImage.size.height)]; // the actual scaling happens here, and orientation is taken care of automatically. CGImageRelease(sourceImg); roundedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); if (!roundedImage) { // Try older method. UIImage *image = bit_imageWithAlpha(inputImage); // Build a context that's the same dimensions as the new size context = CGBitmapContextCreate(NULL, (size_t)image.size.width, (size_t)image.size.height, CGImageGetBitsPerComponent(image.CGImage), 0, CGImageGetColorSpace(image.CGImage), CGImageGetBitmapInfo(image.CGImage)); // Create a clipping path with rounded corners CGContextBeginPath(context); bit_addRoundedRectToPath(CGRectMake(borderSize, borderSize, image.size.width - borderSize * 2, image.size.height - borderSize * 2), context, cornerSize, cornerSize); CGContextClosePath(context); CGContextClip(context); // Draw the image to the context; the clipping path will make anything outside the rounded rect transparent CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage); // Create a CGImage from the context CGImageRef clippedImage = CGBitmapContextCreateImage(context); CGContextRelease(context); // Create a UIImage from the CGImage roundedImage = [UIImage imageWithCGImage:clippedImage]; CGImageRelease(clippedImage); } return roundedImage; } UIImage *bit_appIcon() { NSString *iconString = [NSString string]; NSArray *icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFiles"]; if (!icons) { icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIcons"]; if ((icons) && ([icons isKindOfClass:[NSDictionary class]])) { icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; } if (!icons) { iconString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFile"]; if (!iconString) { iconString = @"Icon.png"; } } } if (icons) { BOOL useHighResIcon = NO; if ([UIScreen mainScreen].scale >= 2) useHighResIcon = YES; for(NSString *icon in icons) { iconString = icon; UIImage *iconImage = [UIImage imageNamed:icon]; if (iconImage.size.height == 57 && !useHighResIcon) { // found! break; } if (iconImage.size.height == 114 && useHighResIcon) { // found! break; } } } BOOL addGloss = YES; NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; if (prerendered) { addGloss = ![prerendered boolValue]; } if (addGloss) { return bit_addGlossToImage([UIImage imageNamed:iconString]); } else { return [UIImage imageNamed:iconString]; } } UIImage *bit_screenshot(void) { // Create a graphics context with the target size CGSize imageSize = [[UIScreen mainScreen] bounds].size; BOOL isLandscapeLeft = [UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationLandscapeLeft; BOOL isLandscapeRight = [UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationLandscapeRight; BOOL isUpsideDown = [UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationPortraitUpsideDown; BOOL needsRotation = NO; if ((isLandscapeLeft ||isLandscapeRight) && imageSize.height > imageSize.width) { needsRotation = YES; CGFloat temp = imageSize.width; imageSize.width = imageSize.height; imageSize.height = temp; } UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0); CGContextRef context = UIGraphicsGetCurrentContext(); // Iterate over every window from back to front //NSInteger count = 0; for (UIWindow *window in [[UIApplication sharedApplication] windows]) { if (![window respondsToSelector:@selector(screen)] || [window screen] == [UIScreen mainScreen]) { // -renderInContext: renders in the coordinate space of the layer, // so we must first apply the layer's geometry to the graphics context CGContextSaveGState(context); // Center the context around the window's anchor point CGContextTranslateCTM(context, [window center].x, [window center].y); // Apply the window's transform about the anchor point CGContextConcatCTM(context, [window transform]); // Offset by the portion of the bounds left of and above the anchor point CGContextTranslateCTM(context, -[window bounds].size.width * [[window layer] anchorPoint].x, -[window bounds].size.height * [[window layer] anchorPoint].y); if (needsRotation) { if (isLandscapeLeft) { CGContextConcatCTM(context, CGAffineTransformRotate(CGAffineTransformMakeTranslation( imageSize.width, 0), (CGFloat)M_PI_2)); } else if (isLandscapeRight) { CGContextConcatCTM(context, CGAffineTransformRotate(CGAffineTransformMakeTranslation( 0, imageSize.height), 3 * (CGFloat)M_PI_2)); } } else if (isUpsideDown) { CGContextConcatCTM(context, CGAffineTransformRotate(CGAffineTransformMakeTranslation( imageSize.width, imageSize.height), (CGFloat)M_PI)); } if ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) { [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:NO]; } else { // Render the layer hierarchy to the current context [[window layer] renderInContext:context]; } // Restore the context CGContextRestoreGState(context); } } // Retrieve the screenshot image UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } #endif /* HOCKEYSDK_CONFIGURATION_ReleaseCrashOnly && HOCKEYSDK_CONFIGURATION_ReleaseCrashOnlyExtensions */