mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00

git-subtree-dir: submodules/HockeySDK-iOS git-subtree-mainline: 085acd26c4432939403765234266e3c1be0f3dd9 git-subtree-split: c7d0c7026303253e2ac576c02655691e5d314fe2
1045 lines
40 KiB
Objective-C
1045 lines
40 KiB
Objective-C
/*
|
|
* Author: Andreas Linde <mail@andreaslinde.de>
|
|
*
|
|
* 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 <QuartzCore/QuartzCore.h>
|
|
#endif
|
|
|
|
#import <tgmath.h>
|
|
#import <sys/sysctl.h>
|
|
|
|
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 */
|